diff --git a/releasenotes/notes/builds_held_attribute-711abd33402ce036.yaml b/releasenotes/notes/builds_held_attribute-711abd33402ce036.yaml new file mode 100644 index 0000000000..eb6507873a --- /dev/null +++ b/releasenotes/notes/builds_held_attribute-711abd33402ce036.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Builds in the SQL reporter have a "held" attribute set to True if the build + triggered a autohold request. Builds can be filtered by held status. +upgrade: + - | + If using a SQL reporter, the zuul_builds table will be updated with a new + 'held' column. The `zuul-scheduler` and `zuul-web` services need to be restarted + together for the change to take effect. diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py index 5e492eabad..f64de8c7c6 100644 --- a/tests/unit/test_connection.py +++ b/tests/unit/test_connection.py @@ -75,7 +75,7 @@ class TestSQLConnection(ZuulDBTestCase): build_table = table_prefix + 'zuul_build' self.assertEqual(16, len(insp.get_columns(buildset_table))) - self.assertEqual(12, len(insp.get_columns(build_table))) + self.assertEqual(13, len(insp.get_columns(build_table))) def test_sql_tables_created(self): "Test the tables for storing results are created properly" diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py index 9916372025..6a74a8a969 100644 --- a/tests/unit/test_web.py +++ b/tests/unit/test_web.py @@ -2080,6 +2080,46 @@ class TestTenantScopedWebApiTokenWithExpiry(BaseTestWeb): self.assertEqual("some reason", ah_request['reason']) +class TestHeldAttributeInBuildInfo(ZuulDBTestCase, BaseTestWeb): + config_file = 'zuul-sql-driver.conf' + tenant_config_file = 'config/sql-driver/main.yaml' + + def test_autohold_and_retrieve_held_build_info(self): + """Ensure the "held" attribute can be used to filter builds""" + client = zuul.rpcclient.RPCClient('127.0.0.1', + self.gearman_server.port) + self.addCleanup(client.shutdown) + r = client.autohold('tenant-one', 'org/project', 'project-test2', + "", "", "reason text", 1) + self.assertTrue(r) + + B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') + self.executor_server.failJob('project-test2', B) + self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1)) + self.waitUntilSettled() + self.executor_server.hold_jobs_in_build = False + self.executor_server.release() + self.waitUntilSettled() + + all_builds_resp = self.get_url("api/tenant/tenant-one/builds?" + "project=org/project") + held_builds_resp = self.get_url("api/tenant/tenant-one/builds?" + "project=org/project&" + "held=1") + self.assertEqual(200, + all_builds_resp.status_code, + all_builds_resp.text) + self.assertEqual(200, + held_builds_resp.status_code, + held_builds_resp.text) + all_builds = all_builds_resp.json() + held_builds = held_builds_resp.json() + self.assertEqual(len(held_builds), 1, all_builds) + held_build = held_builds[0] + self.assertEqual('project-test2', held_build['job_name'], held_build) + self.assertEqual(True, held_build['held'], held_build) + + class TestWebMulti(BaseTestWeb): config_file = 'zuul-gerrit-github.conf' diff --git a/zuul/driver/sql/alembic/versions/32e28a297c3e_add_held_attribute_to_builds.py b/zuul/driver/sql/alembic/versions/32e28a297c3e_add_held_attribute_to_builds.py new file mode 100644 index 0000000000..a9b2f90831 --- /dev/null +++ b/zuul/driver/sql/alembic/versions/32e28a297c3e_add_held_attribute_to_builds.py @@ -0,0 +1,39 @@ +# 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. + +"""add held attribute to builds + +Revision ID: 32e28a297c3e +Revises: 16c1dc9054d0 +Create Date: 2020-05-19 11:19:19.263236 + +""" + +# revision identifiers, used by Alembic. +revision = '32e28a297c3e' +down_revision = '269691d2220e' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(table_prefix=''): + op.add_column( + table_prefix + 'zuul_build', + sa.Column('held', sa.Boolean) + ) + + +def downgrade(): + raise Exception("Downgrades not supported") diff --git a/zuul/driver/sql/sqlconnection.py b/zuul/driver/sql/sqlconnection.py index cd820004f7..aaa5c1d759 100644 --- a/zuul/driver/sql/sqlconnection.py +++ b/zuul/driver/sql/sqlconnection.py @@ -60,7 +60,7 @@ class DatabaseSession(object): change=None, branch=None, patchset=None, ref=None, newrev=None, event_id=None, uuid=None, job_name=None, voting=None, node_name=None, result=None, provides=None, - final=None, limit=50, offset=0): + final=None, held=None, limit=50, offset=0): build_table = self.connection.zuul_build_table buildset_table = self.connection.zuul_buildset_table @@ -104,6 +104,7 @@ class DatabaseSession(object): q = self.listFilter(q, build_table.c.result, result) q = self.listFilter(q, build_table.c.final, final) q = self.listFilter(q, provides_table.c.name, provides) + q = self.listFilter(q, build_table.c.held, held) q = q.order_by(build_table.c.id.desc()).\ limit(limit).\ @@ -296,6 +297,7 @@ class SQLConnection(BaseConnection): uuid = sa.Column(sa.String(36)) job_name = sa.Column(sa.String(255)) result = sa.Column(sa.String(255)) + held = sa.Column(sa.Boolean) start_time = sa.Column(sa.DateTime) end_time = sa.Column(sa.DateTime) voting = sa.Column(sa.Boolean) diff --git a/zuul/driver/sql/sqlreporter.py b/zuul/driver/sql/sqlreporter.py index b77f9e6cf2..9b81b51f38 100644 --- a/zuul/driver/sql/sqlreporter.py +++ b/zuul/driver/sql/sqlreporter.py @@ -59,6 +59,7 @@ class SQLReporter(BaseReporter): node_name=build.node_name, error_detail=build.error_detail, final=final, + held=build.held, ) return db_build @@ -100,7 +101,6 @@ class SQLReporter(BaseReporter): # stats about builds. It doesn't understand how to store # information about the change. continue - retry_builds = item.current_build_set.getRetryBuildsForJob( job.name ) diff --git a/zuul/model.py b/zuul/model.py index cf0191730c..02d72333d1 100644 --- a/zuul/model.py +++ b/zuul/model.py @@ -1882,6 +1882,7 @@ class Build(object): self.canceled = False self.paused = False self.retry = False + self.held = False self.parameters = {} self.worker = Worker() self.node_labels = [] diff --git a/zuul/scheduler.py b/zuul/scheduler.py index 8e0738236b..ae71a72ba0 100644 --- a/zuul/scheduler.py +++ b/zuul/scheduler.py @@ -1565,12 +1565,14 @@ class Scheduler(threading.Thread): # failed / retry_limit / post_failure and have an autohold request. hold_list = ["FAILURE", "RETRY_LIMIT", "POST_FAILURE", "TIMED_OUT"] if build.result not in hold_list: - return + return False request = self._getAutoholdRequest(build) - self.log.debug("Got autohold %s", request) if request is not None: + self.log.debug("Got autohold %s", request) self.nodepool.holdNodeSet(build.nodeset, request, build) + return True + return False def _doBuildCompletedEvent(self, event): build = event.build @@ -1582,7 +1584,10 @@ class Scheduler(threading.Thread): # to pass this on to the pipeline manager, make sure we return # the nodes to nodepool. try: - self._processAutohold(build) + event.build.held = self._processAutohold(build) + self.log.debug( + 'build "%s" held status set to %s' % (event.build, + event.build.held)) except Exception: log.exception("Unable to process autohold for %s" % build) try: diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index 417e5b073e..efa2166971 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -875,6 +875,7 @@ class ZuulWebAPI(object): 'uuid': build.uuid, 'job_name': build.job_name, 'result': build.result, + 'held': build.held, 'start_time': start_time, 'end_time': end_time, 'duration': duration, @@ -934,7 +935,7 @@ class ZuulWebAPI(object): def builds(self, tenant, project=None, pipeline=None, change=None, branch=None, patchset=None, ref=None, newrev=None, uuid=None, job_name=None, voting=None, node_name=None, - result=None, final=None, limit=50, skip=0): + result=None, final=None, held=None, limit=50, skip=0): connection = self._get_connection(tenant) # If final is None, we return all builds, both final and non-final @@ -945,7 +946,7 @@ class ZuulWebAPI(object): tenant=tenant, project=project, pipeline=pipeline, change=change, branch=branch, patchset=patchset, ref=ref, newrev=newrev, uuid=uuid, job_name=job_name, voting=voting, node_name=node_name, - result=result, final=final, limit=limit, offset=skip) + result=result, final=final, held=held, limit=limit, offset=skip) resp = cherrypy.response resp.headers['Access-Control-Allow-Origin'] = '*'