Merge "Add parameterized builds (server)"
This commit is contained in:
commit
a504586684
11 files changed
+450
-9
No files matched your search
@@ -0,0 +1,40 @@
|
||||
- pipeline:
|
||||
name: on-demand
|
||||
manager: independent
|
||||
# allow-parameters: false
|
||||
success:
|
||||
gerrit:
|
||||
Verified: 1
|
||||
failure:
|
||||
gerrit:
|
||||
Verified: -1
|
||||
|
||||
- job:
|
||||
name: base
|
||||
parent: null
|
||||
run: playbooks/base.yaml
|
||||
nodeset:
|
||||
nodes:
|
||||
- label: ubuntu-xenial
|
||||
name: controller
|
||||
|
||||
- job:
|
||||
name: deploy-app
|
||||
run: playbooks/deploy.yaml
|
||||
|
||||
- project:
|
||||
name: org/project
|
||||
on-demand:
|
||||
parameters:
|
||||
- name: target_cluster
|
||||
description: Which cluster to deploy to
|
||||
type: selection
|
||||
values:
|
||||
- production
|
||||
- staging
|
||||
- dev
|
||||
- name: version
|
||||
description: What version to deploy
|
||||
type: string
|
||||
jobs:
|
||||
- deploy-app
|
||||
@@ -0,0 +1,40 @@
|
||||
- pipeline:
|
||||
name: on-demand
|
||||
manager: independent
|
||||
allow-parameters: true
|
||||
success:
|
||||
gerrit:
|
||||
Verified: 1
|
||||
failure:
|
||||
gerrit:
|
||||
Verified: -1
|
||||
|
||||
- job:
|
||||
name: base
|
||||
parent: null
|
||||
run: playbooks/base.yaml
|
||||
nodeset:
|
||||
nodes:
|
||||
- label: ubuntu-xenial
|
||||
name: controller
|
||||
|
||||
- job:
|
||||
name: deploy-app
|
||||
run: playbooks/deploy.yaml
|
||||
|
||||
- project:
|
||||
name: org/project
|
||||
on-demand:
|
||||
parameters:
|
||||
- name: target_cluster
|
||||
description: Which cluster to deploy to
|
||||
type: selection
|
||||
values:
|
||||
- production
|
||||
- staging
|
||||
- dev
|
||||
- name: version
|
||||
description: What version to deploy
|
||||
type: string
|
||||
jobs:
|
||||
- deploy-app
|
||||
@@ -1970,3 +1970,13 @@ class TestNodepoolConfig(LauncherBaseTestCase):
|
||||
self.assertEqual(1, len(layout.loading_errors))
|
||||
self.assertIn('more than the max-retain-count',
|
||||
layout.loading_errors[0].error)
|
||||
|
||||
|
||||
class TestParameterizedBuilds(ZuulTestCase):
|
||||
|
||||
@simple_layout('layouts/parameterized-builds-not-allowed.yaml')
|
||||
def test_parameterized_builds_not_allowed(self):
|
||||
layout = self.scheds.first.sched.abide.tenants.get('tenant-one').layout
|
||||
self.assertEqual(1, len(layout.loading_errors))
|
||||
self.assertIn('does not allow parameters',
|
||||
layout.loading_errors[0].error)
|
||||
@@ -92,7 +92,7 @@ class TestSQLConnectionMysql(ZuulTestCase):
|
||||
buildset_event_table = table_prefix + 'zuul_buildset_event'
|
||||
|
||||
self.assertEqual(9, len(insp.get_columns(ref_table)))
|
||||
self.assertEqual(12, len(insp.get_columns(buildset_table)))
|
||||
self.assertEqual(14, len(insp.get_columns(buildset_table)))
|
||||
self.assertEqual(2, len(insp.get_columns(buildset_ref_table)))
|
||||
self.assertEqual(15, len(insp.get_columns(build_table)))
|
||||
self.assertEqual(5, len(insp.get_columns(artifact_table)))
|
||||
|
||||
@@ -955,9 +955,11 @@ class TestWeb(BaseTestWeb):
|
||||
'pipelines': [{
|
||||
'name': 'check',
|
||||
'jobs': jobs,
|
||||
'parameters': [],
|
||||
}, {
|
||||
'name': 'gate',
|
||||
'jobs': jobs,
|
||||
'parameters': [],
|
||||
}, {'name': 'post',
|
||||
'jobs': [[
|
||||
{'abstract': False,
|
||||
@@ -1010,6 +1012,7 @@ class TestWeb(BaseTestWeb):
|
||||
'workspace_checkout': True,
|
||||
}
|
||||
]],
|
||||
'parameters': [],
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -4197,6 +4200,81 @@ class TestTenantScopedWebApi(BaseTestWeb):
|
||||
dict(name='project-test2', result='SUCCESS', changes='2,1'),
|
||||
], ordered=False)
|
||||
|
||||
@simple_layout('layouts/parameterized-builds.yaml')
|
||||
def test_web_parameterized_build(self):
|
||||
token = self._getToken(['tenant-one'])
|
||||
resp = self.post_url(
|
||||
"api/tenant/tenant-one/project/org/project/freeze-pipeline",
|
||||
json={
|
||||
'ref': 'refs/heads/master',
|
||||
'pipeline': 'on-demand',
|
||||
})
|
||||
data = resp.json()
|
||||
self.assertEqual([
|
||||
{'default': None,
|
||||
'description': 'Which cluster to deploy to',
|
||||
'name': 'target_cluster',
|
||||
'required': False,
|
||||
'type': 'selection',
|
||||
'values': ['production', 'staging', 'dev']},
|
||||
{'default': None,
|
||||
'description': 'What version to deploy',
|
||||
'name': 'version',
|
||||
'required': False,
|
||||
'type': 'string',
|
||||
'values': None},
|
||||
], data['parameters'])
|
||||
|
||||
expected_params = {
|
||||
'target_cluster': 'production',
|
||||
'version': '1.0',
|
||||
}
|
||||
resp = self.post_url(
|
||||
"api/tenant/tenant-one/project/org/project/enqueue",
|
||||
headers={'Authorization': 'Bearer %s' % token},
|
||||
json={
|
||||
'ref': 'refs/heads/master',
|
||||
'pipeline': 'on-demand',
|
||||
'parameters': expected_params,
|
||||
})
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.waitUntilSettled()
|
||||
params = self.builds[0].job.variables
|
||||
self.assertEqual(expected_params, params)
|
||||
|
||||
self.executor_server.hold_jobs_in_build = False
|
||||
self.executor_server.release()
|
||||
self.waitUntilSettled()
|
||||
|
||||
self.assertHistory([
|
||||
dict(name='deploy-app', result='SUCCESS', ref='refs/heads/master'),
|
||||
], ordered=False)
|
||||
|
||||
buildsets = self.get_url("api/tenant/tenant-one/buildsets").json()
|
||||
self.assertEqual(1, len(buildsets))
|
||||
buildset = self.get_url(
|
||||
"api/tenant/tenant-one/buildset/%s" % buildsets[0]['uuid']).json()
|
||||
self.assertEqual(expected_params, buildset['parameters'])
|
||||
self.assertEqual('testuser', buildset['user_id'])
|
||||
|
||||
@simple_layout('layouts/parameterized-builds.yaml')
|
||||
def test_web_parameterized_build_change(self):
|
||||
token = self._getToken(['tenant-one'])
|
||||
expected_params = {
|
||||
'target_cluster': 'production',
|
||||
'version': '1.0',
|
||||
}
|
||||
resp = self.post_url(
|
||||
"api/tenant/tenant-one/project/org/project/enqueue",
|
||||
headers={'Authorization': 'Bearer %s' % token},
|
||||
json={
|
||||
'ref': 'refs/changes/01/1/1',
|
||||
'pipeline': 'on-demand',
|
||||
'parameters': expected_params,
|
||||
})
|
||||
self.assertEqual(400, resp.status_code)
|
||||
self.assertIn('supported only with branches or tags', resp.text)
|
||||
|
||||
|
||||
class TestTenantScopedWebApiWithAccessRules(TestTenantScopedWebApi):
|
||||
config_file = 'zuul-admin-web.conf'
|
||||
|
||||
+39
-1
@@ -1327,10 +1327,23 @@ class ProjectTemplateParser(object):
|
||||
job = {str: vs.Any(str, JobParser.job_attributes)}
|
||||
job_list = [vs.Any(str, job)]
|
||||
|
||||
parameter_types = vs.Any(str, int, float, bool)
|
||||
|
||||
parameter = {
|
||||
vs.Required('name'): ansible_var_name,
|
||||
vs.Required('type'): vs.Any(
|
||||
'string', 'selection', 'multiple-selection', 'bool'),
|
||||
'description': str,
|
||||
'values': [parameter_types],
|
||||
'default': to_list(parameter_types),
|
||||
'required': bool,
|
||||
}
|
||||
|
||||
pipeline_contents = {
|
||||
'debug': bool,
|
||||
'fail-fast': bool,
|
||||
'jobs': job_list,
|
||||
'parameters': to_list(parameter),
|
||||
}
|
||||
|
||||
project = {
|
||||
@@ -1366,6 +1379,15 @@ class ProjectTemplateParser(object):
|
||||
self.parseJobList(
|
||||
conf_pipeline.get('jobs', []),
|
||||
source_context, start_mark, project_pipeline.job_list)
|
||||
for conf_param in conf_pipeline.get('parameters', []):
|
||||
param = model.ProjectParameterConfig()
|
||||
param.name = conf_param['name']
|
||||
param.type = conf_param['type']
|
||||
param.description = conf_param.get('description')
|
||||
param.values = conf_param.get('values')
|
||||
param.default = conf_param.get('default')
|
||||
param.required = conf_param.get('required', False)
|
||||
project_pipeline.parameters[param.name] = param
|
||||
|
||||
# If this project definition is in a place where it
|
||||
# should get implied branch matchers, set it.
|
||||
@@ -1412,10 +1434,23 @@ class ProjectParser(object):
|
||||
job = {str: vs.Any(str, JobParser.job_attributes)}
|
||||
job_list = [vs.Any(str, job)]
|
||||
|
||||
parameter_types = vs.Any(str, int, float, bool)
|
||||
|
||||
parameter = {
|
||||
vs.Required('name'): ansible_var_name,
|
||||
vs.Required('type'): vs.Any(
|
||||
'string', 'selection', 'multiple-selection', 'bool'),
|
||||
'description': str,
|
||||
'values': [parameter_types],
|
||||
'default': to_list(parameter_types),
|
||||
'required': bool,
|
||||
}
|
||||
|
||||
pipeline_contents = {
|
||||
'debug': bool,
|
||||
'fail-fast': bool,
|
||||
'jobs': job_list
|
||||
'jobs': job_list,
|
||||
'parameters': to_list(parameter),
|
||||
}
|
||||
|
||||
project = {
|
||||
@@ -1554,6 +1589,7 @@ class PipelineParser(object):
|
||||
pipeline = {vs.Required('name'): str,
|
||||
vs.Required('manager'): manager,
|
||||
'allow-other-connections': bool,
|
||||
'allow-parameters': bool,
|
||||
'precedence': precedence,
|
||||
'supercedes': to_list(str),
|
||||
'description': str,
|
||||
@@ -1596,6 +1632,8 @@ class PipelineParser(object):
|
||||
pipeline.start_mark = conf['_start_mark']
|
||||
pipeline.allow_other_connections = conf.get(
|
||||
'allow-other-connections', True)
|
||||
pipeline.allow_parameters = conf.get(
|
||||
'allow-parameters', False)
|
||||
pipeline.description = conf.get('description')
|
||||
pipeline.supercedes = as_list(conf.get('supercedes', []))
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# 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.
|
||||
|
||||
"""buildset_parameters
|
||||
|
||||
Revision ID: 1385a2593eb2
|
||||
Revises: 268b2da7a66e
|
||||
Create Date: 2026-05-28 10:03:39.013680
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1385a2593eb2'
|
||||
down_revision = '268b2da7a66e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade(table_prefix=''):
|
||||
op.add_column(
|
||||
table_prefix + 'zuul_buildset',
|
||||
sa.Column('parameters', sa.JSON(), nullable=True))
|
||||
op.add_column(
|
||||
table_prefix + 'zuul_buildset',
|
||||
sa.Column('user_id', sa.String(255), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
raise Exception("Downgrades not supported")
|
||||
@@ -817,6 +817,8 @@ class SQLConnection(BaseConnection):
|
||||
last_build_end_time = sa.Column(sa.DateTime, nullable=True)
|
||||
updated = sa.Column(sa.DateTime, nullable=True)
|
||||
queue_item_uuid = sa.Column(sa.String(36), nullable=True)
|
||||
parameters = sa.Column(sa.JSON(), nullable=True)
|
||||
user_id = sa.Column(sa.String(255), nullable=True)
|
||||
|
||||
refs = orm.relationship(
|
||||
RefModel,
|
||||
|
||||
@@ -56,11 +56,13 @@ class SQLReporter(BaseReporter):
|
||||
def _createBuildset(self, db, buildset):
|
||||
event_id = None
|
||||
event_timestamp = None
|
||||
event_user_id = None
|
||||
item = buildset.item
|
||||
if item.event is not None:
|
||||
event_id = getattr(item.event, "zuul_event_id", None)
|
||||
event_timestamp = datetime.datetime.fromtimestamp(
|
||||
item.event.timestamp, tz=datetime.timezone.utc)
|
||||
event_user_id = getattr(item.event, "user_id", None)
|
||||
|
||||
db_buildset = db.createBuildSet(
|
||||
uuid=buildset.uuid,
|
||||
@@ -70,6 +72,8 @@ class SQLReporter(BaseReporter):
|
||||
event_timestamp=event_timestamp,
|
||||
queue_item_uuid=item.uuid,
|
||||
updated=datetime.datetime.utcnow(),
|
||||
user_id=event_user_id,
|
||||
parameters=item.parameters,
|
||||
)
|
||||
for change in item.changes:
|
||||
ref = db.getOrCreateRef(
|
||||
|
||||
+88
-3
@@ -569,6 +569,7 @@ class Pipeline(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.allow_other_connections = True
|
||||
self.allow_parameters = False
|
||||
self.connections = []
|
||||
self.source_context = None
|
||||
self.start_mark = None
|
||||
@@ -1275,14 +1276,17 @@ class ChangeQueue(zkobject.ZKObject):
|
||||
"Unable to identify triggering ref from event %s",
|
||||
event)
|
||||
event_info = EventInfo.fromEvent(event, event_ref_cache_key)
|
||||
parameters = getattr(event, 'parameters', None)
|
||||
else:
|
||||
event_info = None
|
||||
parameters = None
|
||||
item = QueueItem.new(self.zk_context,
|
||||
queue=self,
|
||||
changes=changes,
|
||||
event=event_info,
|
||||
span_info=span_info,
|
||||
enqueue_time=enqueue_time,
|
||||
parameters=parameters,
|
||||
debug=debug)
|
||||
self.enqueueItem(item)
|
||||
return item
|
||||
@@ -7134,6 +7138,7 @@ class EventInfo:
|
||||
self.image_names = None
|
||||
self.image_upload_uuid = None
|
||||
self.image_build_uuid = None
|
||||
self.user_id = None
|
||||
|
||||
@classmethod
|
||||
def fromEvent(cls, event, event_ref_key):
|
||||
@@ -7148,6 +7153,7 @@ class EventInfo:
|
||||
tinfo.image_names = getattr(event, 'image_names', None)
|
||||
tinfo.image_upload_uuid = getattr(event, 'image_upload_uuid', None)
|
||||
tinfo.image_build_uuid = getattr(event, 'image_build_uuid', None)
|
||||
tinfo.user_id = getattr(event, 'user_id', None)
|
||||
return tinfo
|
||||
|
||||
@classmethod
|
||||
@@ -7160,6 +7166,7 @@ class EventInfo:
|
||||
tinfo.image_names = d.get("image_names")
|
||||
tinfo.image_upload_uuid = d.get("image_upload_uuid")
|
||||
tinfo.image_build_uuid = d.get("image_build_uuid")
|
||||
tinfo.user_id = d.get("user_id")
|
||||
return tinfo
|
||||
|
||||
def isSuperset(self, other):
|
||||
@@ -7195,6 +7202,7 @@ class EventInfo:
|
||||
"image_names": self.image_names,
|
||||
"image_upload_uuid": self.image_upload_uuid,
|
||||
"image_build_uuid": self.image_build_uuid,
|
||||
"user_id": self.user_id,
|
||||
}
|
||||
|
||||
|
||||
@@ -7238,6 +7246,8 @@ class QueueItem(zkobject.ZKObject):
|
||||
_cached_sql_results={},
|
||||
event=None, # Info about the event that lead to this queue item
|
||||
debug=False,
|
||||
user_id=None,
|
||||
parameters=None,
|
||||
reporter_jobs_state=None,
|
||||
# Change.cache_key -> sha for any changes in this item
|
||||
# that were merged
|
||||
@@ -7321,6 +7331,7 @@ class QueueItem(zkobject.ZKObject):
|
||||
"type": event_type,
|
||||
"data": self.event.toDict(),
|
||||
},
|
||||
"parameters": self.parameters,
|
||||
"dynamic_state": self.dynamic_state,
|
||||
"first_job_start_time": self.first_job_start_time,
|
||||
"debug": self.debug,
|
||||
@@ -9097,11 +9108,14 @@ class ChangeManagementEvent(ManagementEvent):
|
||||
:arg str newrev: optional, the new revision
|
||||
:arg dict orig_ref: optional, the cache key reference of the
|
||||
ref of the original triggering event
|
||||
:arg str user_id: optional, event originator user_id
|
||||
:arg dict parameters: optional, pipeline parameters
|
||||
"""
|
||||
|
||||
def __init__(self, tenant_name, pipeline_name, project_hostname,
|
||||
project_name, change=None, ref=None, oldrev=None,
|
||||
newrev=None, orig_ref=None):
|
||||
newrev=None, orig_ref=None, user_id=None,
|
||||
parameters=None):
|
||||
super().__init__()
|
||||
self.type = None
|
||||
self.tenant_name = tenant_name
|
||||
@@ -9120,6 +9134,8 @@ class ChangeManagementEvent(ManagementEvent):
|
||||
self.timestamp = time.time()
|
||||
span = trace.get_current_span()
|
||||
self.span_context = tracing.getSpanContext(span)
|
||||
self.user_id = user_id
|
||||
self.parameters = parameters
|
||||
|
||||
@property
|
||||
def canonical_project_name(self):
|
||||
@@ -9139,6 +9155,8 @@ class ChangeManagementEvent(ManagementEvent):
|
||||
d["timestamp"] = self.timestamp
|
||||
d["span_context"] = self.span_context
|
||||
d["orig_ref"] = self.orig_ref
|
||||
d["user_id"] = self.user_id
|
||||
d["parameters"] = self.parameters
|
||||
return d
|
||||
|
||||
def updateFromDict(self, d):
|
||||
@@ -9159,6 +9177,8 @@ class ChangeManagementEvent(ManagementEvent):
|
||||
data.get("oldrev"),
|
||||
data.get("newrev"),
|
||||
data.get("orig_ref"),
|
||||
data.get("user_id"),
|
||||
data.get("parameters"),
|
||||
)
|
||||
event.updateFromDict(data)
|
||||
return event
|
||||
@@ -9696,6 +9716,28 @@ class TenantProjectConfig(object):
|
||||
return True
|
||||
|
||||
|
||||
class ProjectParameterConfig(ConfigObject):
|
||||
# Represents a project parameter
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = None
|
||||
self.description = None
|
||||
self.type = None
|
||||
self.values = None
|
||||
self.default = None
|
||||
self.required = False
|
||||
|
||||
def toDict(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'type': self.type,
|
||||
'values': self.values,
|
||||
'default': self.default,
|
||||
'required': self.required,
|
||||
}
|
||||
|
||||
|
||||
class ProjectPipelineConfig(ConfigObject):
|
||||
# Represents a project cofiguration in the context of a pipeline
|
||||
def __init__(self):
|
||||
@@ -9705,6 +9747,7 @@ class ProjectPipelineConfig(ConfigObject):
|
||||
self.debug_messages = []
|
||||
self.fail_fast = None
|
||||
self.variables = {}
|
||||
self.parameters = {}
|
||||
|
||||
def addDebug(self, msg):
|
||||
self.debug_messages.append(msg)
|
||||
@@ -9717,6 +9760,9 @@ class ProjectPipelineConfig(ConfigObject):
|
||||
if self.fail_fast is None:
|
||||
self.fail_fast = other.fail_fast
|
||||
self.job_list.inheritFrom(other.job_list)
|
||||
for p in other.parameters.values():
|
||||
if p.name not in self.parameters:
|
||||
self.parameters[p.name] = p
|
||||
|
||||
def updateVariables(self, other):
|
||||
# We need to keep this separate to update() because we wish to
|
||||
@@ -9725,8 +9771,9 @@ class ProjectPipelineConfig(ConfigObject):
|
||||
self.variables = Job._deepUpdate(self.variables, other)
|
||||
|
||||
def toDict(self):
|
||||
d = {}
|
||||
return d
|
||||
return {
|
||||
'parameters': [x.toDict() for x in self.parameters.values()],
|
||||
}
|
||||
|
||||
|
||||
class ProjectConfig(ConfigObject):
|
||||
@@ -10678,10 +10725,21 @@ class Layout(object):
|
||||
|
||||
self.pipeline_managers[manager.pipeline.name] = manager
|
||||
|
||||
def _checkAddPPC(self, pipeline_name, ppc):
|
||||
manager = self.pipeline_managers.get(pipeline_name)
|
||||
# TODO: Consaider raising errors on incorrect pipeline names
|
||||
if manager:
|
||||
pipeline = manager.pipeline
|
||||
if ppc.parameters and not pipeline.allow_parameters:
|
||||
raise Exception("Pipeline does not allow parameters")
|
||||
|
||||
def addProjectTemplate(self, project_template):
|
||||
for job in project_template.embeddedJobs():
|
||||
self._checkAddJob(job)
|
||||
|
||||
for pipeline_name, ppc in project_template.pipelines.items():
|
||||
self._checkAddPPC(pipeline_name, ppc)
|
||||
|
||||
template_list = self.project_templates.get(project_template.name)
|
||||
if template_list is not None:
|
||||
reference = template_list[0]
|
||||
@@ -10710,6 +10768,9 @@ class Layout(object):
|
||||
for job in project_config.embeddedJobs():
|
||||
self._checkAddJob(job)
|
||||
|
||||
for pipeline_name, ppc in project_config.pipelines.items():
|
||||
self._checkAddPPC(pipeline_name, ppc)
|
||||
|
||||
if project_canonical_name in self.project_configs:
|
||||
self.project_configs[project_canonical_name].append(project_config)
|
||||
else:
|
||||
@@ -10911,6 +10972,25 @@ class Layout(object):
|
||||
raise NoMatchingParentError()
|
||||
return jobs
|
||||
|
||||
def collectParameters(self, item, ppc):
|
||||
# Filter supplied parameters against the parameter
|
||||
# specifications in the ppc
|
||||
parameters = {}
|
||||
if not item.parameters:
|
||||
return parameters
|
||||
for (k, v) in item.parameters.items():
|
||||
param_def = ppc.parameters.get(k)
|
||||
if param_def:
|
||||
if param_def.type == 'multiple-selection':
|
||||
if isinstance(v, list) and set(v) <= set(param_def.values):
|
||||
parameters[k] = v
|
||||
elif param_def.type == 'selection':
|
||||
if v in param_def.values:
|
||||
parameters[k] = v
|
||||
else:
|
||||
parameters[k] = v
|
||||
return parameters
|
||||
|
||||
def extendJobGraph(self, context, item, change, ppc, job_graph,
|
||||
skip_file_matcher, redact_secrets_and_keys,
|
||||
debug_messages, pending_errors):
|
||||
@@ -10958,6 +11038,11 @@ class Layout(object):
|
||||
final_job.name = variant.name
|
||||
final_job.name = jobname
|
||||
|
||||
# Don't accept any parameters that aren't specified in our PPC
|
||||
parameters = self.collectParameters(item, ppc)
|
||||
# Pipeline parameters behave like project variables, but
|
||||
# have higher precedence
|
||||
final_job.updateProjectVariables(parameters)
|
||||
# Now merge variables set from this parent ppc
|
||||
# (i.e. project+templates) directly into the job vars
|
||||
final_job.updateProjectVariables(ppc.variables)
|
||||
|
||||
+107
-4
@@ -65,7 +65,9 @@ from zuul.model import (
|
||||
PromoteEvent,
|
||||
ProviderNode,
|
||||
QueueItem,
|
||||
Ref,
|
||||
SystemAttributes,
|
||||
Tag,
|
||||
UnparsedAbideConfig,
|
||||
WebInfo,
|
||||
)
|
||||
@@ -357,6 +359,8 @@ class BuildsetConverter:
|
||||
'queue_item_uuid': buildset.queue_item_uuid,
|
||||
'first_build_start_time': start,
|
||||
'last_build_end_time': end,
|
||||
'user_id': buildset.user_id,
|
||||
'parameters': buildset.parameters,
|
||||
'refs': [
|
||||
RefConverter.toDict(ref)
|
||||
for ref in buildset.refs
|
||||
@@ -379,6 +383,8 @@ class BuildsetConverter:
|
||||
'event_timestamp': str,
|
||||
'first_build_start_time': str,
|
||||
'last_build_end_time': str,
|
||||
'user_id': str,
|
||||
'parameters': dict,
|
||||
'refs': [
|
||||
RefConverter.schema()
|
||||
],
|
||||
@@ -807,6 +813,42 @@ class SystemEventConverter:
|
||||
return Prop('The system event', ret)
|
||||
|
||||
|
||||
class ProjectPipelineConverter:
|
||||
# A class to encapsulate the conversion of project pipeline
|
||||
# config objects to API output.
|
||||
def toDict(ppc):
|
||||
parameters = []
|
||||
if ppc:
|
||||
for p in ppc.parameters.values():
|
||||
parameters.append({
|
||||
'name': p.name,
|
||||
'type': p.type,
|
||||
'description': p.description,
|
||||
'values': p.values,
|
||||
'default': p.default,
|
||||
'required': p.required,
|
||||
})
|
||||
ret = {
|
||||
'parameters': parameters,
|
||||
}
|
||||
return ret
|
||||
|
||||
def schema():
|
||||
parameter = {
|
||||
'name': str,
|
||||
'type': str,
|
||||
'description': str,
|
||||
'values': [str],
|
||||
'default': str,
|
||||
'required': bool,
|
||||
}
|
||||
|
||||
ret = {
|
||||
'parameters': [parameter],
|
||||
}
|
||||
return Prop('The project pipeline config', ret)
|
||||
|
||||
|
||||
class APIError(cherrypy.HTTPError):
|
||||
def __init__(self, code, json_doc=None, headers=None):
|
||||
self._headers = headers or {}
|
||||
@@ -1523,13 +1565,21 @@ class ZuulWebAPI(object):
|
||||
if manager is None:
|
||||
raise cherrypy.HTTPError(400, 'Unknown pipeline')
|
||||
|
||||
if body.get('parameters'):
|
||||
ref = body.get('ref', '')
|
||||
if not (ref.startswith('refs/heads') or
|
||||
ref.startswith('refs/tags')):
|
||||
raise cherrypy.HTTPError(
|
||||
400, 'Parameters are supported only with branches or tags')
|
||||
|
||||
if 'change' in body:
|
||||
return self._enqueue(tenant, project, manager.pipeline,
|
||||
body['change'])
|
||||
elif all(p in body for p in ['ref', 'oldrev', 'newrev']):
|
||||
elif 'ref' in body:
|
||||
return self._enqueue_ref(tenant, project,
|
||||
manager.pipeline, body['ref'],
|
||||
body['oldrev'], body['newrev'])
|
||||
body.get('oldrev'), body.get('newrev'),
|
||||
auth.uid, body.get('parameters'))
|
||||
else:
|
||||
raise cherrypy.HTTPError(400, 'Invalid request body')
|
||||
|
||||
@@ -1543,11 +1593,13 @@ class ZuulWebAPI(object):
|
||||
|
||||
return True
|
||||
|
||||
def _enqueue_ref(self, tenant, project, pipeline, ref, oldrev, newrev):
|
||||
def _enqueue_ref(self, tenant, project, pipeline, ref, oldrev,
|
||||
newrev, user_id, parameters):
|
||||
event = EnqueueEvent(tenant.name, pipeline.name,
|
||||
project.canonical_hostname, project.name,
|
||||
change=None, ref=ref, oldrev=oldrev,
|
||||
newrev=newrev)
|
||||
newrev=newrev, user_id=user_id,
|
||||
parameters=parameters)
|
||||
event.zuul_event_id = get_zuul_request_id()
|
||||
self.zuulweb.pipeline_management_events[tenant.name][
|
||||
pipeline.name].put(event)
|
||||
@@ -3376,6 +3428,50 @@ class ZuulWebAPI(object):
|
||||
|
||||
return item, change
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.save_params()
|
||||
@cherrypy.tools.json_in()
|
||||
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
|
||||
@cherrypy.tools.handle_options()
|
||||
@cherrypy.tools.check_tenant_auth()
|
||||
@openapi_response(
|
||||
code=200,
|
||||
content_type='application/json',
|
||||
description='Returns the project pipeline config',
|
||||
schema=ProjectPipelineConverter.schema(),
|
||||
)
|
||||
def project_freeze_pipeline(self, tenant_name, tenant, auth,
|
||||
project_name):
|
||||
body = cherrypy.request.json
|
||||
if 'ref' not in body:
|
||||
raise cherrypy.HTTPError(400, 'ref is required')
|
||||
if 'pipeline' not in body:
|
||||
raise cherrypy.HTTPError(400, 'pipeline is required')
|
||||
|
||||
project = self._getProjectOrRaise(tenant, project_name)
|
||||
manager = tenant.layout.pipeline_managers.get(body['pipeline'])
|
||||
if not manager:
|
||||
raise cherrypy.HTTPError(404, 'Unknown pipeline')
|
||||
|
||||
# refs/heads/...
|
||||
ref = body['ref'].split('/', 2)
|
||||
if ref[1] == 'heads':
|
||||
change = Branch(project)
|
||||
change.branch = ref[2]
|
||||
elif ref[1] == 'tags':
|
||||
change = Tag(project)
|
||||
change.tag = ref[2]
|
||||
else:
|
||||
change = Ref(project)
|
||||
change.ref = body['ref']
|
||||
|
||||
change.cache_stat = FakeCacheKey()
|
||||
with LocalZKContext(self.log) as context:
|
||||
queue = ChangeQueue.new(context, manager=manager)
|
||||
item = QueueItem.new(context, queue=queue, changes=[change])
|
||||
ppc = tenant.layout.getProjectPipelineConfig(item, change)
|
||||
return ProjectPipelineConverter.toDict(ppc)
|
||||
|
||||
|
||||
class StaticHandler(object):
|
||||
def __init__(self, root):
|
||||
@@ -3593,6 +3689,13 @@ class ZuulWeb(object):
|
||||
'api',
|
||||
'/api/tenant/{tenant_name}/project/{project_name:.*}/dequeue',
|
||||
controller=api, action='dequeue')
|
||||
route_map.connect(
|
||||
'api',
|
||||
'/api/tenant/{tenant_name}/project/{project_name:.*}'
|
||||
'/freeze-pipeline',
|
||||
controller=api,
|
||||
action='project_freeze_pipeline',
|
||||
)
|
||||
route_map.connect('api',
|
||||
'/api/tenant/{tenant_name}/autohold/{request_id}',
|
||||
controller=api,
|
||||
|
||||
Reference in new issue
Block a user