mirror of
https://pagure.io/fm-orchestrator.git
synced 2026-02-13 01:54:59 +08:00
Fix #105 - Add new cancel API call which can be used to cancel the module build.
This commit is contained in:
@@ -231,6 +231,15 @@ class GenericBuilder(six.with_metaclass(ABCMeta)):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def cancel_build(self, task_id):
|
||||
"""
|
||||
:param task_id: Task ID returned by the build method.
|
||||
|
||||
Cancels the build.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def repo_from_tag(self, config, tag_name, arch):
|
||||
@@ -568,6 +577,9 @@ chmod 644 %buildroot/%_rpmconfigdir/macros.d/macros.modules
|
||||
reason = "Failed to submit artifact %s to Koji" % (artifact_name)
|
||||
return task_id, state, reason, None
|
||||
|
||||
def cancel_build(self, task_id):
|
||||
self.koji_session.cancelTask(task_id)
|
||||
|
||||
@classmethod
|
||||
def repo_from_tag(cls, config, tag_name, arch):
|
||||
"""
|
||||
@@ -910,6 +922,8 @@ class CoprModuleBuilder(GenericBuilder):
|
||||
raise ValueError(response["error"])
|
||||
return response["repo"]
|
||||
|
||||
def cancel_build(self, task_id):
|
||||
pass
|
||||
|
||||
class MockModuleBuilder(GenericBuilder):
|
||||
"""
|
||||
@@ -1216,6 +1230,9 @@ $repos
|
||||
# @FIXME
|
||||
return KojiModuleBuilder.get_disttag_srpm(disttag)
|
||||
|
||||
def cancel_build(self, task_id):
|
||||
pass
|
||||
|
||||
GenericBuilder.register_backend_class(KojiModuleBuilder)
|
||||
GenericBuilder.register_backend_class(CoprModuleBuilder)
|
||||
GenericBuilder.register_backend_class(MockModuleBuilder)
|
||||
|
||||
@@ -44,6 +44,57 @@ def get_rpm_release_from_tag(tag):
|
||||
def get_artifact_from_srpm(srpm_path):
|
||||
return os.path.basename(srpm_path).replace(".src.rpm", "")
|
||||
|
||||
def failed(config, session, msg):
|
||||
"""
|
||||
Called whenever a module enters the 'failed' state.
|
||||
|
||||
We cancel all the remaining component builds of a module
|
||||
and stop the building.
|
||||
"""
|
||||
|
||||
build = models.ModuleBuild.from_module_event(session, msg)
|
||||
|
||||
module_info = build.json()
|
||||
if module_info['state'] != msg.module_build_state:
|
||||
log.warn("Note that retrieved module state %r "
|
||||
"doesn't match message module state %r" % (
|
||||
module_info['state'], msg.module_build_state))
|
||||
# This is ok.. it's a race condition we can ignore.
|
||||
pass
|
||||
|
||||
unbuilt_components = [
|
||||
c for c in build.component_builds
|
||||
if (c.state != koji.BUILD_STATES['COMPLETE']
|
||||
and c.state != koji.BUILD_STATES["FAILED"])
|
||||
]
|
||||
|
||||
try:
|
||||
groups = {
|
||||
'build': build.resolve_profiles(session, 'buildroot'),
|
||||
'srpm-build': build.resolve_profiles(session, 'srpm-buildroot'),
|
||||
}
|
||||
except ValueError:
|
||||
reason = "Failed to gather buildroot groups from SCM."
|
||||
log.exception(reason)
|
||||
build.transition(config, state="failed", state_reason=reason)
|
||||
session.commit()
|
||||
raise
|
||||
|
||||
builder = module_build_service.builder.GenericBuilder.create(
|
||||
build.owner, build.name, config.system, config, tag_name=build.koji_tag)
|
||||
builder.buildroot_connect(groups)
|
||||
|
||||
for component in unbuilt_components:
|
||||
if component.task_id:
|
||||
builder.cancel_build(component.task_id)
|
||||
component.state = koji.BUILD_STATES['FAILED']
|
||||
component.state_reason = build.state_reason
|
||||
session.add(component)
|
||||
|
||||
build.transition(config, state="failed")
|
||||
session.commit()
|
||||
|
||||
|
||||
def done(config, session, msg):
|
||||
"""Called whenever a module enters the 'done' state.
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class MessageWorker(threading.Thread):
|
||||
models.BUILD_STATES["init"]: NO_OP,
|
||||
models.BUILD_STATES["wait"]: module_build_service.scheduler.handlers.modules.wait,
|
||||
models.BUILD_STATES["build"]: NO_OP,
|
||||
models.BUILD_STATES["failed"]: NO_OP,
|
||||
models.BUILD_STATES["failed"]: module_build_service.scheduler.handlers.modules.failed,
|
||||
models.BUILD_STATES["done"]: module_build_service.scheduler.handlers.modules.done, # XXX: DIRECT TRANSITION TO READY
|
||||
models.BUILD_STATES["ready"]: NO_OP,
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ from flask import request, jsonify
|
||||
from flask.views import MethodView
|
||||
|
||||
from module_build_service import app, conf, log
|
||||
from module_build_service import models
|
||||
from module_build_service import models, db
|
||||
from module_build_service.utils import pagination_metadata, filter_module_builds, submit_module_build
|
||||
from module_build_service.errors import (
|
||||
ValidationError, Unauthorized, NotFound)
|
||||
@@ -60,6 +60,12 @@ api_v1 = {
|
||||
'methods': ['GET'],
|
||||
}
|
||||
},
|
||||
'module_build_cancel': {
|
||||
'url': '/module-build-service/1/module-builds/cancel/<int:id>',
|
||||
'options': {
|
||||
'methods': ['PUT']
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +132,32 @@ class ModuleBuildAPI(MethodView):
|
||||
module = submit_module_build(username, url)
|
||||
return jsonify(module.json()), 201
|
||||
|
||||
def put(self, id):
|
||||
username = module_build_service.auth.get_username(request.environ)
|
||||
|
||||
if conf.require_packager:
|
||||
module_build_service.auth.assert_is_packager(username, fas_kwargs=dict(
|
||||
base_url=conf.fas_url,
|
||||
username=conf.fas_username,
|
||||
password=conf.fas_password))
|
||||
|
||||
if id is None:
|
||||
raise NotFound('You must provide module build id.')
|
||||
|
||||
module = models.ModuleBuild.query.filter_by(id=id).first()
|
||||
if not module:
|
||||
raise NotFound('No such module found.')
|
||||
|
||||
if module.owner != username:
|
||||
raise Unauthorized("You are not owner of this build and "
|
||||
"therefore cannot cancel it.")
|
||||
|
||||
module.transition(conf, models.BUILD_STATES["failed"],
|
||||
"Canceled by %s." % username)
|
||||
db.session.add(module)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(module.api_json()), 200
|
||||
|
||||
def register_api_v1():
|
||||
""" Registers version 1 of Rida API. """
|
||||
|
||||
@@ -77,11 +77,22 @@ class TestModuleBuilder(GenericBuilder):
|
||||
# Global build_id/task_id we increment when new build is executed.
|
||||
_build_id = 1
|
||||
|
||||
BUILD_STATE = "COMPLETE"
|
||||
|
||||
on_build_cb = None
|
||||
on_cancel_cb = None
|
||||
|
||||
def __init__(self, owner, module, config, tag_name):
|
||||
self.module_str = module
|
||||
self.tag_name = tag_name
|
||||
self.config = config
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
TestModuleBuilder.BUILD_STATE = "COMPLETE"
|
||||
TestModuleBuilder.on_build_cb = None
|
||||
TestModuleBuilder.on_cancel_cb = None
|
||||
|
||||
def buildroot_connect(self, groups):
|
||||
pass
|
||||
|
||||
@@ -129,10 +140,15 @@ class TestModuleBuilder(GenericBuilder):
|
||||
|
||||
TestModuleBuilder._build_id += 1
|
||||
|
||||
self._send_repo_done()
|
||||
self._send_build_change(koji.BUILD_STATES['COMPLETE'], source,
|
||||
TestModuleBuilder._build_id)
|
||||
self._send_repo_done()
|
||||
if TestModuleBuilder.BUILD_STATE != "BUILDING":
|
||||
self._send_repo_done()
|
||||
self._send_build_change(
|
||||
koji.BUILD_STATES[TestModuleBuilder.BUILD_STATE], source,
|
||||
TestModuleBuilder._build_id)
|
||||
self._send_repo_done()
|
||||
|
||||
if TestModuleBuilder.on_build_cb:
|
||||
TestModuleBuilder.on_build_cb(self, artifact_name, source)
|
||||
|
||||
state = koji.BUILD_STATES['BUILDING']
|
||||
reason = "Submitted %s to Koji" % (artifact_name)
|
||||
@@ -143,6 +159,10 @@ class TestModuleBuilder(GenericBuilder):
|
||||
# @FIXME
|
||||
return KojiModuleBuilder.get_disttag_srpm(disttag)
|
||||
|
||||
def cancel_build(self, task_id):
|
||||
if TestModuleBuilder.on_cancel_cb:
|
||||
TestModuleBuilder.on_cancel_cb(self, task_id)
|
||||
|
||||
def set_dburi(dburi):
|
||||
"""
|
||||
Sets database URI in all places in the middle of test.
|
||||
@@ -175,6 +195,7 @@ class TestBuild(unittest.TestCase):
|
||||
# Set back the original database URI
|
||||
set_dburi(self.orig_dburi)
|
||||
conf.set_item("system", "koji")
|
||||
TestModuleBuilder.reset()
|
||||
|
||||
@patch('module_build_service.auth.get_username', return_value='Homer J. Simpson')
|
||||
@patch('module_build_service.auth.assert_is_packager')
|
||||
@@ -203,3 +224,53 @@ class TestBuild(unittest.TestCase):
|
||||
for build in models.ComponentBuild.query.filter_by(module_id=module_build_id).all():
|
||||
self.assertEqual(build.state, koji.BUILD_STATES['COMPLETE'])
|
||||
self.assertTrue(build.module_build.state in [models.BUILD_STATES["done"], models.BUILD_STATES["ready"]] )
|
||||
|
||||
@patch('module_build_service.auth.get_username', return_value='Homer J. Simpson')
|
||||
@patch('module_build_service.auth.assert_is_packager')
|
||||
@patch('module_build_service.scm.SCM')
|
||||
def test_submit_build_cancel(self, mocked_scm, mocked_assert_is_packager,
|
||||
mocked_get_username):
|
||||
"""
|
||||
Submit all builds for a module and cancel the module build later.
|
||||
"""
|
||||
mocked_scm_obj = MockedSCM(mocked_scm, "testmodule", "testmodule.yaml")
|
||||
|
||||
rv = self.client.post('/module-build-service/1/module-builds/', data=json.dumps(
|
||||
{'scmurl': 'git://pkgs.stg.fedoraproject.org/modules/'
|
||||
'testmodule.git?#68932c90de214d9d13feefbd35246a81b6cb8d49'}))
|
||||
|
||||
data = json.loads(rv.data)
|
||||
module_build_id = data['id']
|
||||
|
||||
# This callback is called before return of TestModuleBuilder.build()
|
||||
# method. We just cancel the build here using the web API to simulate
|
||||
# user cancelling the build in the middle of building.
|
||||
def on_build_cb(cls, artifact_name, source):
|
||||
self.client.put('/module-build-service/1/module-builds/cancel/' + str(module_build_id))
|
||||
|
||||
cancelled_tasks = []
|
||||
def on_cancel_cb(cls, task_id):
|
||||
cancelled_tasks.append(task_id)
|
||||
|
||||
# We do not want the builds to COMPLETE, but instead we want them
|
||||
# to be in the BULDING state after the TestModuleBuilder.build().
|
||||
TestModuleBuilder.BUILD_STATE = "BUILDING"
|
||||
TestModuleBuilder.on_build_cb = on_build_cb
|
||||
TestModuleBuilder.on_cancel_cb = on_cancel_cb
|
||||
|
||||
msgs = []
|
||||
msgs.append(RidaModule("fake msg", 1, 1))
|
||||
module_build_service.scheduler.main.main(msgs, True)
|
||||
|
||||
# Because we did not finished single component build and canceled the
|
||||
# module build, all components and even the module itself should be in
|
||||
# failed state with state_reason se to cancellation message.
|
||||
for build in models.ComponentBuild.query.filter_by(module_id=module_build_id).all():
|
||||
self.assertEqual(build.state, koji.BUILD_STATES['FAILED'])
|
||||
self.assertEqual(build.state_reason, "Canceled by Homer J. Simpson.")
|
||||
self.assertEqual(build.module_build.state, models.BUILD_STATES["failed"])
|
||||
self.assertEqual(build.module_build.state_reason, "Canceled by Homer J. Simpson.")
|
||||
|
||||
# Check that cancel_build has been called for this build
|
||||
if build.task_id:
|
||||
self.assertTrue(build.task_id in cancelled_tasks)
|
||||
|
||||
@@ -401,4 +401,23 @@ class TestViews(unittest.TestCase):
|
||||
self.assertEquals(batches["bash"], 2)
|
||||
self.assertEquals(batches["file"], 3)
|
||||
|
||||
@patch('module_build_service.auth.get_username', return_value='some_other_user')
|
||||
@patch('module_build_service.auth.assert_is_packager')
|
||||
def test_cancel_build(self, mocked_assert_is_packager,
|
||||
mocked_get_username):
|
||||
rv = self.client.put('/module-build-service/1/module-builds/cancel/30')
|
||||
data = json.loads(rv.data)
|
||||
|
||||
self.assertEquals(data['state'], 4)
|
||||
self.assertEquals(data['state_reason'], 'Canceled by some_other_user.')
|
||||
|
||||
|
||||
@patch('module_build_service.auth.get_username', return_value='Someone else')
|
||||
@patch('module_build_service.auth.assert_is_packager')
|
||||
def test_cancel_build_unauthorized(self, mocked_assert_is_packager,
|
||||
mocked_get_username):
|
||||
rv = self.client.put('/module-build-service/1/module-builds/cancel/30')
|
||||
data = json.loads(rv.data)
|
||||
|
||||
self.assertEquals(data['status'], 401)
|
||||
self.assertEquals(data['error'], 'Unauthorized')
|
||||
|
||||
Reference in New Issue
Block a user