Function versioning API: get

Support 2 types of queries:

- Get basic function version information
- Download package of that specific function version

The previous function downloading process is not affected.

TODO:

- Test of downloading function version package will be added in
  functional test(in the subsequent patch)

Change-Id: I1bcb7c13a7b65432e1d6714d7539077ab82ee831
Story: #2001829
Task: #14335
This commit is contained in:
Lingxian Kong 2018-04-19 00:26:55 +12:00
parent 9005ceadae
commit e8e8084982
8 changed files with 129 additions and 10 deletions

View File

@ -129,7 +129,6 @@ class FunctionsController(rest.RestController):
pecan.response.headers['Content-Disposition'] = ( pecan.response.headers['Content-Disposition'] = (
'attachment; filename="%s"' % os.path.basename(func_db.name) 'attachment; filename="%s"' % os.path.basename(func_db.name)
) )
LOG.info("Downloaded function %s", id)
@rest_utils.wrap_pecan_controller_exception @rest_utils.wrap_pecan_controller_exception
@pecan.expose('json') @pecan.expose('json')

View File

@ -12,10 +12,15 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import collections
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import strutils
import pecan
from pecan import rest from pecan import rest
import tenacity import tenacity
from webob.static import FileIter
import wsmeext.pecan as wsme_pecan import wsmeext.pecan as wsme_pecan
from qinling.api import access_control as acl from qinling.api import access_control as acl
@ -137,3 +142,38 @@ class FunctionVersionsController(rest.RestController):
for v in db_versions] for v in db_versions]
return resources.FunctionVersions(function_versions=versions) return resources.FunctionVersions(function_versions=versions)
@rest_utils.wrap_pecan_controller_exception
@pecan.expose()
def get(self, function_id, version):
ctx = context.get_ctx()
acl.enforce('function_version:get', ctx)
download = strutils.bool_from_string(
pecan.request.GET.get('download', False)
)
version = int(version)
version_db = db_api.get_function_version(function_id, version)
if not download:
LOG.info("Getting version %s for function %s.", version,
function_id)
pecan.override_template('json')
return resources.FunctionVersion.from_dict(
version_db.to_dict()).to_dict()
LOG.info("Downloading version %s for function %s.", version,
function_id)
f = self.storage_provider.retrieve(ctx.projectid, function_id,
None, version=version)
if isinstance(f, collections.Iterable):
pecan.response.app_iter = f
else:
pecan.response.app_iter = FileIter(f)
pecan.response.headers['Content-Type'] = 'application/zip'
pecan.response.headers['Content-Disposition'] = (
'attachment; filename="%s_%s"' % (function_id, version)
)

View File

@ -206,3 +206,7 @@ def delete_webhooks(**kwargs):
def increase_function_version(function_id, old_version, **kwargs): def increase_function_version(function_id, old_version, **kwargs):
"""This function is meant to be invoked within locking section.""" """This function is meant to be invoked within locking section."""
return IMPL.increase_function_version(function_id, old_version, **kwargs) return IMPL.increase_function_version(function_id, old_version, **kwargs)
def get_function_version(function_id, version):
return IMPL.get_function_version(function_id, version)

View File

@ -514,3 +514,16 @@ def increase_function_version(function_id, old_version, session=None,
) )
return version return version
@db_base.session_aware()
def get_function_version(function_id, version, session=None):
version_db = _secure_query(models.FunctionVersion).filter_by(
function_id=function_id, version_number=version).first()
if not version_db:
raise exc.DBEntityNotFoundError(
"FunctionVersion not found [function_id=%s, version_number=%s]" %
(function_id, version)
)
return version_db

View File

@ -40,12 +40,13 @@ class PackageStorage(object):
raise NotImplementedError raise NotImplementedError
@abc.abstractmethod @abc.abstractmethod
def retrieve(self, project_id, function, md5sum): def retrieve(self, project_id, function, md5sum, version=None):
"""Get function package data. """Get function package data.
:param project_id: Project ID. :param project_id: Project ID.
:param function: Function ID. :param function: Function ID.
:param md5sum: The function MD5. :param md5sum: The function MD5.
:param version: Optional. The function version number.
:return: File descriptor that needs to close outside. :return: File descriptor that needs to close outside.
""" """
raise NotImplementedError raise NotImplementedError

View File

@ -74,23 +74,42 @@ class FileSystemStorage(base.PackageStorage):
os.rename(new_func_zip, func_zip) os.rename(new_func_zip, func_zip)
return md5_actual return md5_actual
def retrieve(self, project_id, function, md5sum): def retrieve(self, project_id, function, md5sum, version=0):
"""Get function package data. """Get function package data.
If version is not 0, return the package data of that specific function
version.
:param project_id: Project ID. :param project_id: Project ID.
:param function: Function ID. :param function: Function ID.
:param md5sum: The function MD5. :param md5sum: The function MD5.
:param version: Optional. The function version number.
:return: File descriptor that needs to close outside. :return: File descriptor that needs to close outside.
""" """
LOG.debug( LOG.debug(
'Getting package data, function: %s, md5sum: %s, project: %s', 'Getting package data, function: %s, version: %s, md5sum: %s, '
function, md5sum, project_id 'project: %s',
function, md5sum, version, project_id
) )
func_zip = os.path.join( if version != 0:
self.base_path, project_dir = os.path.join(self.base_path, project_id)
PACKAGE_PATH_TEMPLATE % (project_id, function, md5sum) for filename in os.listdir(project_dir):
) root, ext = os.path.splitext(filename)
if (root.startswith("%s_%d" % (function, version))
and ext == '.zip'):
func_zip = os.path.join(project_dir, filename)
break
else:
raise exc.StorageNotFoundException(
'Package of version %d function %s for project %s not '
'found.' % (version, function, project_id)
)
else:
func_zip = os.path.join(
self.base_path,
PACKAGE_PATH_TEMPLATE % (project_id, function, md5sum)
)
if not os.path.exists(func_zip): if not os.path.exists(func_zip):
raise exc.StorageNotFoundException( raise exc.StorageNotFoundException(
@ -99,7 +118,8 @@ class FileSystemStorage(base.PackageStorage):
) )
f = open(func_zip, 'rb') f = open(func_zip, 'rb')
LOG.debug('Found package data for function %s', function) LOG.debug('Found package data for function %s version %d', function,
version)
return f return f

View File

@ -108,3 +108,12 @@ class TestFunctionVersionController(base.APITest):
actual = self._assert_single_item(resp.json['function_versions'], actual = self._assert_single_item(resp.json['function_versions'],
version_number=1) version_number=1)
self.assertEqual("version 1", actual.get('description')) self.assertEqual("version 1", actual.get('description'))
def test_get(self):
db_api.increase_function_version(self.func_id, 0,
description="version 1")
resp = self.app.get('/v1/functions/%s/versions/1' % self.func_id)
self.assertEqual(200, resp.status_int)
self.assertEqual("version 1", resp.json.get('description'))

View File

@ -150,6 +150,39 @@ class TestFileSystemStorage(base.BaseTest):
) )
exists_mock.assert_called_once_with(package_path) exists_mock.assert_called_once_with(package_path)
@mock.patch('qinling.storage.file_system.open')
@mock.patch('os.path.exists')
@mock.patch('os.listdir')
def test_retrieve_version(self, mock_list, mock_exist, mock_open):
function = "fake_function_id"
version = 1
md5 = "md5"
mock_list.return_value = ["%s_%s_%s.zip" % (function, version, md5)]
mock_exist.return_value = True
self.storage.retrieve(self.project_id, function, None,
version=version)
version_zip = os.path.join(FAKE_STORAGE_PATH, self.project_id,
"%s_%s_%s.zip" % (function, version, md5))
mock_exist.assert_called_once_with(version_zip)
@mock.patch('os.listdir')
def test_retrieve_version_not_found(self, mock_list):
function = "fake_function_id"
version = 1
mock_list.return_value = [""]
self.assertRaises(
exc.StorageNotFoundException,
self.storage.retrieve,
function,
self.project_id,
None,
version=version
)
@mock.patch('os.path.exists') @mock.patch('os.path.exists')
@mock.patch('os.remove') @mock.patch('os.remove')
def test_delete(self, remove_mock, exists_mock): def test_delete(self, remove_mock, exists_mock):