Delete all its versions when deleting function
Change-Id: Ic0132619b0aa513cd84abc278006623b42ef15b1 Story: 2001829 Task: 14423
This commit is contained in:
parent
e0e8f3d13d
commit
3f303eb60c
@ -254,7 +254,10 @@ class FunctionsController(rest.RestController):
|
|||||||
@rest_utils.wrap_wsme_controller_exception
|
@rest_utils.wrap_wsme_controller_exception
|
||||||
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||||
def delete(self, id):
|
def delete(self, id):
|
||||||
"""Delete the specified function."""
|
"""Delete the specified function.
|
||||||
|
|
||||||
|
Delete function will also delete all its versions.
|
||||||
|
"""
|
||||||
LOG.info("Delete function %s.", id)
|
LOG.info("Delete function %s.", id)
|
||||||
|
|
||||||
with db_api.transaction():
|
with db_api.transaction():
|
||||||
@ -263,9 +266,9 @@ class FunctionsController(rest.RestController):
|
|||||||
raise exc.NotAllowedException(
|
raise exc.NotAllowedException(
|
||||||
'The function is still associated with running job(s).'
|
'The function is still associated with running job(s).'
|
||||||
)
|
)
|
||||||
if func_db.webhook:
|
if len(func_db.webhooks) > 0:
|
||||||
raise exc.NotAllowedException(
|
raise exc.NotAllowedException(
|
||||||
'The function is still associated with webhook.'
|
'The function is still associated with webhook(s).'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Even admin user can not delete other project's function because
|
# Even admin user can not delete other project's function because
|
||||||
@ -275,22 +278,41 @@ class FunctionsController(rest.RestController):
|
|||||||
'Function can only be deleted by its owner.'
|
'Function can only be deleted by its owner.'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Delete trust if needed
|
||||||
|
if func_db.trust_id:
|
||||||
|
keystone_util.delete_trust(func_db.trust_id)
|
||||||
|
|
||||||
|
for version_db in func_db.versions:
|
||||||
|
# Delete all resources created by orchestrator asynchronously.
|
||||||
|
self.engine_client.delete_function(
|
||||||
|
id,
|
||||||
|
version=version_db.version_number
|
||||||
|
)
|
||||||
|
# Delete etcd keys
|
||||||
|
etcd_util.delete_function(
|
||||||
|
id,
|
||||||
|
version=version_db.version_number
|
||||||
|
)
|
||||||
|
# Delete function version packages. Versions is only supported
|
||||||
|
# for package type function.
|
||||||
|
self.storage_provider.delete(
|
||||||
|
func_db.project_id,
|
||||||
|
id,
|
||||||
|
None,
|
||||||
|
version=version_db.version_number
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete resources for function version 0(func_db.versions==[])
|
||||||
|
self.engine_client.delete_function(id)
|
||||||
|
etcd_util.delete_function(id)
|
||||||
|
|
||||||
source = func_db.code['source']
|
source = func_db.code['source']
|
||||||
if source == constants.PACKAGE_FUNCTION:
|
if source == constants.PACKAGE_FUNCTION:
|
||||||
self.storage_provider.delete(func_db.project_id, id,
|
self.storage_provider.delete(func_db.project_id, id,
|
||||||
func_db.code['md5sum'])
|
func_db.code['md5sum'])
|
||||||
|
|
||||||
# Delete all resources created by orchestrator asynchronously.
|
# This will also delete function service mapping and function
|
||||||
self.engine_client.delete_function(id)
|
# versions as well.
|
||||||
|
|
||||||
# Delete trust if needed
|
|
||||||
if func_db.trust_id:
|
|
||||||
keystone_util.delete_trust(func_db.trust_id)
|
|
||||||
|
|
||||||
# Delete etcd keys
|
|
||||||
etcd_util.delete_function(id)
|
|
||||||
|
|
||||||
# This will also delete function service mapping as well.
|
|
||||||
db_api.delete_function(id)
|
db_api.delete_function(id)
|
||||||
|
|
||||||
@rest_utils.wrap_pecan_controller_exception
|
@rest_utils.wrap_pecan_controller_exception
|
||||||
|
@ -56,7 +56,6 @@ def transaction():
|
|||||||
|
|
||||||
def delete_all():
|
def delete_all():
|
||||||
"""A helper function for testing."""
|
"""A helper function for testing."""
|
||||||
with transaction():
|
|
||||||
delete_jobs(insecure=True)
|
delete_jobs(insecure=True)
|
||||||
delete_webhooks(insecure=True)
|
delete_webhooks(insecure=True)
|
||||||
delete_executions(insecure=True)
|
delete_executions(insecure=True)
|
||||||
|
@ -193,11 +193,11 @@ def _get_db_object_by_id(model, id, insecure=None):
|
|||||||
|
|
||||||
@db_base.insecure_aware()
|
@db_base.insecure_aware()
|
||||||
def _delete_all(model, insecure=None, **kwargs):
|
def _delete_all(model, insecure=None, **kwargs):
|
||||||
# NOTE(kong): Because we use 'in_' operator in _secure_query(), delete()
|
# NOTE(kong): If we use 'in_' operator in _secure_query(), delete()
|
||||||
# method will raise error with default parameter. Please refer to
|
# method will raise error with default parameter. Please refer to
|
||||||
# http://docs.sqlalchemy.org/en/rel_1_0/orm/query.html#sqlalchemy.orm.query.Query.delete
|
# http://docs.sqlalchemy.org/en/rel_1_0/orm/query.html#sqlalchemy.orm.query.Query.delete
|
||||||
query = db_base.model_query(model) if insecure else _secure_query(model)
|
query = db_base.model_query(model) if insecure else _secure_query(model)
|
||||||
query.filter_by(**kwargs).delete(synchronize_session=False)
|
query.filter_by(**kwargs).delete(synchronize_session="fetch")
|
||||||
|
|
||||||
|
|
||||||
@db_base.session_aware()
|
@db_base.session_aware()
|
||||||
|
@ -134,7 +134,7 @@ Function.jobs = relationship(
|
|||||||
"~Job.status.in_(['done', 'cancelled']))"
|
"~Job.status.in_(['done', 'cancelled']))"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Function.webhook = relationship("Webhook", uselist=False, backref="function")
|
Function.webhooks = relationship("Webhook", uselist=True, backref="function")
|
||||||
Function.versions = relationship(
|
Function.versions = relationship(
|
||||||
"FunctionVersion",
|
"FunctionVersion",
|
||||||
order_by="FunctionVersion.version_number",
|
order_by="FunctionVersion.version_number",
|
||||||
|
@ -48,6 +48,7 @@ def handle_function_service_expiration(ctx, engine):
|
|||||||
delta = timedelta(seconds=CONF.engine.function_service_expiration)
|
delta = timedelta(seconds=CONF.engine.function_service_expiration)
|
||||||
expiry_time = datetime.utcnow() - delta
|
expiry_time = datetime.utcnow() - delta
|
||||||
|
|
||||||
|
with db_api.transaction():
|
||||||
results = db_api.get_functions(
|
results = db_api.get_functions(
|
||||||
sort_keys=['updated_at'],
|
sort_keys=['updated_at'],
|
||||||
insecure=True,
|
insecure=True,
|
||||||
@ -60,7 +61,8 @@ def handle_function_service_expiration(ctx, engine):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
LOG.info(
|
LOG.info(
|
||||||
'Deleting service mapping and workers for function %s(version 0)',
|
'Deleting service mapping and workers for function '
|
||||||
|
'%s(version 0)',
|
||||||
func_db.id
|
func_db.id
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -80,7 +82,8 @@ def handle_function_service_expiration(ctx, engine):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
LOG.info(
|
LOG.info(
|
||||||
'Deleting service mapping and workers for function %s(version %s)',
|
'Deleting service mapping and workers for function '
|
||||||
|
'%s(version %s)',
|
||||||
v.function_id, v.version_number
|
v.function_id, v.version_number
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -92,11 +95,18 @@ def handle_function_service_expiration(ctx, engine):
|
|||||||
|
|
||||||
@periodics.periodic(3)
|
@periodics.periodic(3)
|
||||||
def handle_job(engine_client):
|
def handle_job(engine_client):
|
||||||
"""Execute job task with no db transactions."""
|
"""Execute job task with no db transactions.
|
||||||
for job in db_api.get_next_jobs(timeutils.utcnow() + timedelta(seconds=3)):
|
|
||||||
job_id = job.id
|
We don't do iterations on jobs_db directly to avoid the potential
|
||||||
func_id = job.function_id
|
'Cursor needed to be reset' error.
|
||||||
func_version = job.function_version
|
"""
|
||||||
|
jobs_db = db_api.get_next_jobs(timeutils.utcnow() + timedelta(seconds=3))
|
||||||
|
jobs_dict = [j.to_dict() for j in jobs_db]
|
||||||
|
|
||||||
|
for job in jobs_dict:
|
||||||
|
job_id = job["id"]
|
||||||
|
func_id = job["function_id"]
|
||||||
|
func_version = job["function_version"]
|
||||||
LOG.debug("Processing job: %s, function: %s(version %s)", job_id,
|
LOG.debug("Processing job: %s, function: %s(version %s)", job_id,
|
||||||
func_id, func_version)
|
func_id, func_version)
|
||||||
|
|
||||||
@ -106,16 +116,16 @@ def handle_job(engine_client):
|
|||||||
try:
|
try:
|
||||||
# Setup context before schedule job.
|
# Setup context before schedule job.
|
||||||
ctx = keystone_utils.create_trust_context(
|
ctx = keystone_utils.create_trust_context(
|
||||||
trust_id, job.project_id
|
trust_id, job["project_id"]
|
||||||
)
|
)
|
||||||
context.set_ctx(ctx)
|
context.set_ctx(ctx)
|
||||||
|
|
||||||
if (job.count is not None and job.count > 0):
|
if (job["count"] is not None and job["count"] > 0):
|
||||||
job.count -= 1
|
job["count"] -= 1
|
||||||
|
|
||||||
# Job delete/update is done using UPDATE ... FROM ... WHERE
|
# Job delete/update is done using UPDATE ... FROM ... WHERE
|
||||||
# non-locking clause.
|
# non-locking clause.
|
||||||
if job.count == 0:
|
if job["count"] == 0:
|
||||||
modified = db_api.conditional_update(
|
modified = db_api.conditional_update(
|
||||||
models.Job,
|
models.Job,
|
||||||
{
|
{
|
||||||
@ -130,19 +140,19 @@ def handle_job(engine_client):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
next_time = jobs.get_next_execution_time(
|
next_time = jobs.get_next_execution_time(
|
||||||
job.pattern,
|
job["pattern"],
|
||||||
job.next_execution_time
|
job["next_execution_time"]
|
||||||
)
|
)
|
||||||
|
|
||||||
modified = db_api.conditional_update(
|
modified = db_api.conditional_update(
|
||||||
models.Job,
|
models.Job,
|
||||||
{
|
{
|
||||||
'next_execution_time': next_time,
|
'next_execution_time': next_time,
|
||||||
'count': job.count
|
'count': job["count"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'id': job_id,
|
'id': job_id,
|
||||||
'next_execution_time': job.next_execution_time
|
'next_execution_time': job["next_execution_time"]
|
||||||
},
|
},
|
||||||
insecure=True,
|
insecure=True,
|
||||||
)
|
)
|
||||||
@ -162,7 +172,7 @@ def handle_job(engine_client):
|
|||||||
params = {
|
params = {
|
||||||
'function_id': func_id,
|
'function_id': func_id,
|
||||||
'function_version': func_version,
|
'function_version': func_version,
|
||||||
'input': job.function_input,
|
'input': job["function_input"],
|
||||||
'sync': False,
|
'sync': False,
|
||||||
'description': constants.EXECUTION_BY_JOB % job_id
|
'description': constants.EXECUTION_BY_JOB % job_id
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import tempfile
|
|||||||
import mock
|
import mock
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from qinling.db import api as db_api
|
||||||
from qinling import status
|
from qinling import status
|
||||||
from qinling.tests.unit.api import base
|
from qinling.tests.unit.api import base
|
||||||
from qinling.tests.unit import base as unit_base
|
from qinling.tests.unit import base as unit_base
|
||||||
@ -277,6 +278,64 @@ class TestFunctionController(base.APITest):
|
|||||||
|
|
||||||
self.assertEqual(403, resp.status_int)
|
self.assertEqual(403, resp.status_int)
|
||||||
|
|
||||||
|
@mock.patch('qinling.utils.etcd_util.delete_function')
|
||||||
|
@mock.patch('qinling.rpc.EngineClient.delete_function')
|
||||||
|
@mock.patch('qinling.storage.file_system.FileSystemStorage.delete')
|
||||||
|
def test_delete_with_versions(self, mock_package_delete,
|
||||||
|
mock_engine_delete, mock_etcd_delete):
|
||||||
|
db_func = self.create_function(runtime_id=self.runtime_id)
|
||||||
|
func_id = db_func.id
|
||||||
|
# Create two versions for the function
|
||||||
|
db_api.increase_function_version(func_id, 0)
|
||||||
|
db_api.increase_function_version(func_id, 1)
|
||||||
|
|
||||||
|
resp = self.app.delete('/v1/functions/%s' % func_id)
|
||||||
|
|
||||||
|
self.assertEqual(204, resp.status_int)
|
||||||
|
|
||||||
|
self.assertEqual(3, mock_package_delete.call_count)
|
||||||
|
self.assertEqual(3, mock_engine_delete.call_count)
|
||||||
|
self.assertEqual(3, mock_etcd_delete.call_count)
|
||||||
|
|
||||||
|
mock_package_delete.assert_has_calls(
|
||||||
|
[
|
||||||
|
mock.call(unit_base.DEFAULT_PROJECT_ID, func_id, None,
|
||||||
|
version=1),
|
||||||
|
mock.call(unit_base.DEFAULT_PROJECT_ID, func_id, None,
|
||||||
|
version=2),
|
||||||
|
mock.call(unit_base.DEFAULT_PROJECT_ID, func_id, "fake_md5")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_engine_delete.assert_has_calls(
|
||||||
|
[
|
||||||
|
mock.call(func_id, version=1),
|
||||||
|
mock.call(func_id, version=2),
|
||||||
|
mock.call(func_id)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_etcd_delete.assert_has_calls(
|
||||||
|
[
|
||||||
|
mock.call(func_id, version=1),
|
||||||
|
mock.call(func_id, version=2),
|
||||||
|
mock.call(func_id)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_delete_with_version_associate_webhook(self):
|
||||||
|
db_func = self.create_function(runtime_id=self.runtime_id)
|
||||||
|
func_id = db_func.id
|
||||||
|
db_api.increase_function_version(func_id, 0)
|
||||||
|
self.create_webhook(func_id, function_version=1)
|
||||||
|
|
||||||
|
resp = self.app.delete(
|
||||||
|
'/v1/functions/%s' % func_id,
|
||||||
|
expect_errors=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(403, resp.status_int)
|
||||||
|
|
||||||
@mock.patch('qinling.rpc.EngineClient.scaleup_function')
|
@mock.patch('qinling.rpc.EngineClient.scaleup_function')
|
||||||
def test_scale_up(self, scaleup_function_mock):
|
def test_scale_up(self, scaleup_function_mock):
|
||||||
db_func = self.create_function(runtime_id=self.runtime_id)
|
db_func = self.create_function(runtime_id=self.runtime_id)
|
||||||
|
Loading…
Reference in New Issue
Block a user