Functional test for function version detach/get
- Only admin user can detach function version. - Admin user has read access to user's function version. Change-Id: I345a2aa17d7131984038f99cdd6e6f246bec1d24 Story: 2001829 Task: 14456
This commit is contained in:
parent
0e8d91c30b
commit
19cd85e818
|
@ -100,7 +100,13 @@ class FunctionsController(rest.RestController):
|
||||||
|
|
||||||
@rest_utils.wrap_pecan_controller_exception
|
@rest_utils.wrap_pecan_controller_exception
|
||||||
@pecan.expose()
|
@pecan.expose()
|
||||||
|
@pecan.expose('json')
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
|
"""Get function information or download function package.
|
||||||
|
|
||||||
|
This method can support HTTP request using either
|
||||||
|
'Accept:application/json' or no 'Accept' header.
|
||||||
|
"""
|
||||||
LOG.info("Get function %s.", id)
|
LOG.info("Get function %s.", id)
|
||||||
|
|
||||||
download = strutils.bool_from_string(
|
download = strutils.bool_from_string(
|
||||||
|
|
|
@ -65,7 +65,7 @@ class FunctionVersionsController(rest.RestController):
|
||||||
|
|
||||||
with db_api.transaction():
|
with db_api.transaction():
|
||||||
# Get latest function package md5 and version number
|
# Get latest function package md5 and version number
|
||||||
func_db = db_api.get_function(function_id)
|
func_db = db_api.get_function(function_id, insecure=False)
|
||||||
if func_db.code['source'] != constants.PACKAGE_FUNCTION:
|
if func_db.code['source'] != constants.PACKAGE_FUNCTION:
|
||||||
raise exc.NotAllowedException(
|
raise exc.NotAllowedException(
|
||||||
"Function versioning only allowed for %s type "
|
"Function versioning only allowed for %s type "
|
||||||
|
@ -140,8 +140,12 @@ class FunctionVersionsController(rest.RestController):
|
||||||
@rest_utils.wrap_wsme_controller_exception
|
@rest_utils.wrap_wsme_controller_exception
|
||||||
@wsme_pecan.wsexpose(resources.FunctionVersions, types.uuid)
|
@wsme_pecan.wsexpose(resources.FunctionVersions, types.uuid)
|
||||||
def get_all(self, function_id):
|
def get_all(self, function_id):
|
||||||
|
"""Get all the versions of the given function.
|
||||||
|
|
||||||
|
Admin user can get all versions for the normal user's function.
|
||||||
|
"""
|
||||||
acl.enforce('function_version:get_all', context.get_ctx())
|
acl.enforce('function_version:get_all', context.get_ctx())
|
||||||
LOG.info("Getting function versions for function %s.", function_id)
|
LOG.info("Getting versions for function %s.", function_id)
|
||||||
|
|
||||||
# Getting function and versions needs to happen in a db transaction
|
# Getting function and versions needs to happen in a db transaction
|
||||||
with db_api.transaction():
|
with db_api.transaction():
|
||||||
|
@ -155,7 +159,13 @@ class FunctionVersionsController(rest.RestController):
|
||||||
|
|
||||||
@rest_utils.wrap_pecan_controller_exception
|
@rest_utils.wrap_pecan_controller_exception
|
||||||
@pecan.expose()
|
@pecan.expose()
|
||||||
|
@pecan.expose('json')
|
||||||
def get(self, function_id, version):
|
def get(self, function_id, version):
|
||||||
|
"""Get function version or download function version package.
|
||||||
|
|
||||||
|
This method can support HTTP request using either
|
||||||
|
'Accept:application/json' or no 'Accept' header.
|
||||||
|
"""
|
||||||
ctx = context.get_ctx()
|
ctx = context.get_ctx()
|
||||||
acl.enforce('function_version:get', ctx)
|
acl.enforce('function_version:get', ctx)
|
||||||
|
|
||||||
|
@ -175,7 +185,7 @@ class FunctionVersionsController(rest.RestController):
|
||||||
LOG.info("Downloading version %s for function %s.", version,
|
LOG.info("Downloading version %s for function %s.", version,
|
||||||
function_id)
|
function_id)
|
||||||
|
|
||||||
f = self.storage_provider.retrieve(ctx.projectid, function_id,
|
f = self.storage_provider.retrieve(version_db.project_id, function_id,
|
||||||
None, version=version)
|
None, version=version)
|
||||||
|
|
||||||
if isinstance(f, collections.Iterable):
|
if isinstance(f, collections.Iterable):
|
||||||
|
|
|
@ -516,10 +516,18 @@ def increase_function_version(function_id, old_version, session=None,
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
@db_base.insecure_aware()
|
||||||
@db_base.session_aware()
|
@db_base.session_aware()
|
||||||
def get_function_version(function_id, version, session=None):
|
def get_function_version(function_id, version, session=None, insecure=None):
|
||||||
version_db = _secure_query(models.FunctionVersion).filter_by(
|
if insecure:
|
||||||
function_id=function_id, version_number=version).first()
|
query = db_base.model_query(models.FunctionVersion)
|
||||||
|
else:
|
||||||
|
query = _secure_query(models.FunctionVersion)
|
||||||
|
|
||||||
|
version_db = query.filter_by(
|
||||||
|
function_id=function_id, version_number=version
|
||||||
|
).first()
|
||||||
|
|
||||||
if not version_db:
|
if not version_db:
|
||||||
raise exc.DBEntityNotFoundError(
|
raise exc.DBEntityNotFoundError(
|
||||||
"FunctionVersion not found [function_id=%s, version_number=%s]" %
|
"FunctionVersion not found [function_id=%s, version_number=%s]" %
|
||||||
|
|
|
@ -106,14 +106,25 @@ class QinlingClient(client_base.QinlingClientBase):
|
||||||
|
|
||||||
return resp, json.loads(resp.text)
|
return resp, json.loads(resp.text)
|
||||||
|
|
||||||
|
def get_function(self, function_id):
|
||||||
|
resp, body = self.get(
|
||||||
|
'/v1/functions/{id}'.format(id=function_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
return resp, json.loads(body)
|
||||||
|
|
||||||
def download_function(self, function_id):
|
def download_function(self, function_id):
|
||||||
return self.get('/v1/functions/%s?download=true' % function_id,
|
return self.get('/v1/functions/%s?download=true' % function_id,
|
||||||
headers={})
|
headers={})
|
||||||
|
|
||||||
def detach_function(self, function_id):
|
def detach_function(self, function_id, version=0):
|
||||||
return self.post('/v1/functions/%s/detach' % function_id,
|
if version == 0:
|
||||||
None,
|
url = '/v1/functions/%s/detach' % function_id
|
||||||
headers={})
|
else:
|
||||||
|
url = '/v1/functions/%s/versions/%s/detach' % \
|
||||||
|
(function_id, version)
|
||||||
|
|
||||||
|
return self.post(url, None, headers={})
|
||||||
|
|
||||||
def create_execution(self, function_id, input=None, sync=True, version=0):
|
def create_execution(self, function_id, input=None, sync=True, version=0):
|
||||||
req_body = {
|
req_body = {
|
||||||
|
@ -130,8 +141,16 @@ class QinlingClient(client_base.QinlingClientBase):
|
||||||
return self.get('/v1/executions/%s/log' % execution_id,
|
return self.get('/v1/executions/%s/log' % execution_id,
|
||||||
headers={'Accept': 'text/plain'})
|
headers={'Accept': 'text/plain'})
|
||||||
|
|
||||||
def get_function_workers(self, function_id):
|
def get_function_workers(self, function_id, version=0):
|
||||||
return self.get_resources('functions/%s/workers' % function_id)
|
q_params = None
|
||||||
|
if version > 0:
|
||||||
|
q_params = "/?function_version=%s" % version
|
||||||
|
|
||||||
|
url = 'functions/%s/workers' % function_id
|
||||||
|
if q_params:
|
||||||
|
url += q_params
|
||||||
|
|
||||||
|
return self.get_resources(url)
|
||||||
|
|
||||||
def create_webhook(self, function_id, version=0):
|
def create_webhook(self, function_id, version=0):
|
||||||
req_body = {"function_id": function_id, "function_version": version}
|
req_body = {"function_id": function_id, "function_version": version}
|
||||||
|
@ -174,3 +193,17 @@ class QinlingClient(client_base.QinlingClientBase):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def get_function_version(self, function_id, version):
|
||||||
|
resp, body = self.get(
|
||||||
|
'/v1/functions/%s/versions/%s' % (function_id, version),
|
||||||
|
)
|
||||||
|
|
||||||
|
return resp, json.loads(body)
|
||||||
|
|
||||||
|
def get_function_versions(self, function_id):
|
||||||
|
resp, body = self.get(
|
||||||
|
'/v1/functions/%s/versions' % (function_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
return resp, json.loads(body)
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
from tempest.lib import decorators
|
from tempest.lib import decorators
|
||||||
from tempest.lib import exceptions
|
from tempest.lib import exceptions
|
||||||
|
import tenacity
|
||||||
|
|
||||||
from qinling_tempest_plugin.tests import base
|
from qinling_tempest_plugin.tests import base
|
||||||
|
|
||||||
|
@ -70,3 +71,81 @@ class FunctionVersionsTest(base.BaseQinlingTest):
|
||||||
numbers = [v['version_number'] for v in body['function_versions']]
|
numbers = [v['version_number'] for v in body['function_versions']]
|
||||||
self.assertIn(version_1, numbers)
|
self.assertIn(version_1, numbers)
|
||||||
self.assertIn(version_2, numbers)
|
self.assertIn(version_2, numbers)
|
||||||
|
|
||||||
|
@decorators.idempotent_id('3f735ed4-64b0-4ec3-8bf2-507e38dcea19')
|
||||||
|
def test_create_admin_not_allowed(self):
|
||||||
|
"""test_create_admin_not_allowed
|
||||||
|
|
||||||
|
Even admin user can not create function version for normal user's
|
||||||
|
function.
|
||||||
|
"""
|
||||||
|
function_id = self.create_function()
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
exceptions.NotFound,
|
||||||
|
self.admin_client.create_function_version,
|
||||||
|
function_id
|
||||||
|
)
|
||||||
|
|
||||||
|
@decorators.idempotent_id('43c06f41-d116-43a7-a61c-115f7591b22e')
|
||||||
|
def test_get_by_admin(self):
|
||||||
|
"""Admin user can get normal user's function version."""
|
||||||
|
function_id = self.create_function()
|
||||||
|
version = self.create_function_version(function_id)
|
||||||
|
|
||||||
|
resp, body = self.admin_client.get_function_version(function_id,
|
||||||
|
version)
|
||||||
|
|
||||||
|
self.assertEqual(200, resp.status)
|
||||||
|
self.assertEqual(version, body.get("version_number"))
|
||||||
|
|
||||||
|
@decorators.idempotent_id('e6b865d8-ffa8-4cfc-8afb-820c64f9b2af')
|
||||||
|
def test_get_all_by_admin(self):
|
||||||
|
"""Admin user can list normal user's function version."""
|
||||||
|
function_id = self.create_function()
|
||||||
|
version = self.create_function_version(function_id)
|
||||||
|
|
||||||
|
resp, body = self.admin_client.get_function_versions(function_id)
|
||||||
|
|
||||||
|
self.assertEqual(200, resp.status)
|
||||||
|
self.assertIn(
|
||||||
|
version,
|
||||||
|
[v['version_number'] for v in body['function_versions']]
|
||||||
|
)
|
||||||
|
|
||||||
|
@decorators.idempotent_id('7898f89f-a490-42a3-8cf7-63cbd9543a06')
|
||||||
|
def test_detach(self):
|
||||||
|
"""Admin only operation."""
|
||||||
|
function_id = self.create_function()
|
||||||
|
version = self.create_function_version(function_id)
|
||||||
|
|
||||||
|
# Create execution to allocate worker
|
||||||
|
resp, _ = self.client.create_execution(
|
||||||
|
function_id, input='{"name": "Qinling"}', version=version
|
||||||
|
)
|
||||||
|
self.assertEqual(201, resp.status)
|
||||||
|
|
||||||
|
resp, body = self.admin_client.get_function_workers(function_id,
|
||||||
|
version=version)
|
||||||
|
self.assertEqual(200, resp.status)
|
||||||
|
self.assertEqual(1, len(body['workers']))
|
||||||
|
|
||||||
|
# Detach function version from workers
|
||||||
|
resp, _ = self.admin_client.detach_function(function_id,
|
||||||
|
version=version)
|
||||||
|
self.assertEqual(202, resp.status)
|
||||||
|
|
||||||
|
def _assert_workers():
|
||||||
|
resp, body = self.admin_client.get_function_workers(
|
||||||
|
function_id,
|
||||||
|
version=version
|
||||||
|
)
|
||||||
|
self.assertEqual(200, resp.status)
|
||||||
|
self.assertEqual(0, len(body['workers']))
|
||||||
|
|
||||||
|
r = tenacity.Retrying(
|
||||||
|
wait=tenacity.wait_fixed(1),
|
||||||
|
stop=tenacity.stop_after_attempt(5),
|
||||||
|
retry=tenacity.retry_if_exception_type(AssertionError)
|
||||||
|
)
|
||||||
|
r.call(_assert_workers)
|
||||||
|
|
|
@ -68,12 +68,27 @@ class FunctionsTest(base.BaseQinlingTest):
|
||||||
|
|
||||||
self.assertEqual(400, resp.status_code)
|
self.assertEqual(400, resp.status_code)
|
||||||
|
|
||||||
@decorators.idempotent_id('051f3106-df01-4fcd-a0a3-c81c99653163')
|
@decorators.idempotent_id('f8dde7fc-fbcc-495c-9b39-70666b7d3f64')
|
||||||
def test_get_all_admin(self):
|
def test_get_by_admin(self):
|
||||||
# Create function by normal user
|
"""test_get_by_admin
|
||||||
|
|
||||||
|
Admin user can get the function by directly specifying the function id.
|
||||||
|
"""
|
||||||
|
function_id = self.create_function(self.python_zip_file)
|
||||||
|
|
||||||
|
resp, body = self.admin_client.get_function(function_id)
|
||||||
|
|
||||||
|
self.assertEqual(200, resp.status)
|
||||||
|
self.assertEqual(function_id, body['id'])
|
||||||
|
|
||||||
|
@decorators.idempotent_id('051f3106-df01-4fcd-a0a3-c81c99653163')
|
||||||
|
def test_get_all_admin(self):
|
||||||
|
"""test_get_all_admin
|
||||||
|
|
||||||
|
Admin user needs to specify filters to get all the functions.
|
||||||
|
"""
|
||||||
function_id = self.create_function(self.python_zip_file)
|
function_id = self.create_function(self.python_zip_file)
|
||||||
|
|
||||||
# Get functions by admin
|
|
||||||
resp, body = self.admin_client.get_resources('functions')
|
resp, body = self.admin_client.get_resources('functions')
|
||||||
|
|
||||||
self.assertEqual(200, resp.status)
|
self.assertEqual(200, resp.status)
|
||||||
|
@ -82,7 +97,6 @@ class FunctionsTest(base.BaseQinlingTest):
|
||||||
[function['id'] for function in body['functions']]
|
[function['id'] for function in body['functions']]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get other projects functions by admin
|
|
||||||
resp, body = self.admin_client.get_resources(
|
resp, body = self.admin_client.get_resources(
|
||||||
'functions?all_projects=true'
|
'functions?all_projects=true'
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue