config/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/kube_app.py

326 lines
12 KiB
Python

#
# Copyright (c) 2018 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import os
import pecan
from pecan import rest
import shutil
import tempfile
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from contextlib import contextmanager
from sysinv import objects
from sysinv.api.controllers.v1 import base
from sysinv.api.controllers.v1 import collection
from sysinv.api.controllers.v1 import types
from sysinv.api.controllers.v1 import utils
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common import utils as cutils
from sysinv.openstack.common import log
from sysinv.openstack.common.gettextutils import _
LOG = log.getLogger(__name__)
@contextmanager
def TempDirectory():
tmpdir = tempfile.mkdtemp()
saved_umask = os.umask(0077)
try:
yield tmpdir
finally:
LOG.debug("Cleaning up temp directory %s" % tmpdir)
os.umask(saved_umask)
shutil.rmtree(tmpdir)
class KubeApp(base.APIBase):
"""API representation of a containerized application."""
id = int
"Unique ID for this application"
name = wtypes.text
"Represents the name of the application"
created_at = wtypes.datetime.datetime
"Represents the time the application was uploaded"
updated_at = wtypes.datetime.datetime
"Represents the time the application was updated"
manifest_name = wtypes.text
"Represents the name of the application manifest"
manifest_file = wtypes.text
"Represents the filename of the application manifest"
status = wtypes.text
"Represents the installation status of the application"
progress = wtypes.text
"Represents the installation progress of the application"
def __init__(self, **kwargs):
self.fields = objects.kube_app.fields.keys()
for k in self.fields:
if not hasattr(self, k):
continue
setattr(self, k, kwargs.get(k, wtypes.Unset))
@classmethod
def convert_with_links(cls, rpc_app, expand=True):
app = KubeApp(**rpc_app.as_dict())
if not expand:
app.unset_fields_except(['name', 'manifest_name',
'manifest_file', 'status', 'progress'])
# skip the id
app.id = wtypes.Unset
return app
class KubeAppCollection(collection.Collection):
"""API representation of a collection of Helm applications."""
apps = [KubeApp]
"A list containing application objects"
def __init__(self, **kwargs):
self._type = 'apps'
@classmethod
def convert_with_links(cls, rpc_apps, expand=False):
collection = KubeAppCollection()
collection.apps = [KubeApp.convert_with_links(n, expand)
for n in rpc_apps]
return collection
LOCK_NAME = 'KubeAppController'
class KubeAppController(rest.RestController):
"""REST controller for Helm applications."""
def __init__(self, parent=None, **kwargs):
self._parent = parent
def _check_environment(self):
if not utils.is_kubernetes_config():
raise exception.OperationNotPermitted
def _check_tarfile(self, app_name, app_tarfile):
if app_name and app_tarfile:
if not os.path.isfile(app_tarfile):
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: application tar file {} does "
"not exist.".format(app_tarfile)))
if (not app_tarfile.endswith('.tgz') and
not app_tarfile.endswith('.tar.gz')):
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: {} has unrecognizable tar file "
"extension. Supported extensions are: .tgz and .tar.gz.".format(
app_tarfile)))
with TempDirectory() as app_path:
if not cutils.extract_tarfile(app_path, app_tarfile):
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: failed to extract tar file "
"{}.".format(os.path.basename(app_tarfile))))
# If checksum file is included in the tarball, verify its contents.
if not cutils.verify_checksum(app_path):
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: checksum validation failed."))
mname, mfile = self._find_manifest_file(app_path)
charts_dir = os.path.join(app_path, 'charts')
if os.path.isdir(charts_dir):
tar_filelist = cutils.get_files_matching(app_path, '.tgz')
if len(os.listdir(charts_dir)) == 0:
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: tar file contains no "
"Helm charts."))
if not tar_filelist:
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: tar file contains no "
"Helm charts of expected file extension (.tgz)."))
for p, f in tar_filelist:
if not cutils.extract_tarfile(p, os.path.join(p, f)):
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: failed to extract tar "
"file {}.".format(os.path.basename(f))))
LOG.info("Tar file of application %s verified." % app_name)
return mname, mfile
else:
raise ValueError(_(
"Application-upload rejected: both application name and tar "
"file must be specified."))
def _find_manifest_file(self, app_path):
# It is expected that there is only one manifest file
# per application and the file exists at top level of
# the application path.
mfiles = cutils.find_manifest_file(app_path)
if mfiles is None:
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: manifest file is corrupted."))
if mfiles:
if len(mfiles) == 1:
return mfiles[0]
else:
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: tar file contains more "
"than one manifest file."))
else:
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: manifest file is missing."))
def _get_one(self, app_name):
# can result in KubeAppNotFound
kube_app = objects.kube_app.get_by_name(
pecan.request.context, app_name)
return KubeApp.convert_with_links(kube_app)
@wsme_pecan.wsexpose(KubeAppCollection)
def get_all(self):
self._check_environment()
apps = pecan.request.dbapi.kube_app_get_all()
return KubeAppCollection.convert_with_links(apps)
@wsme_pecan.wsexpose(KubeApp, wtypes.text)
def get_one(self, app_name):
"""Retrieve a single application."""
self._check_environment()
return self._get_one(app_name)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(KubeApp, body=types.apidict)
def post(self, body):
"""Uploading an application to be deployed by Armada"""
self._check_environment()
name = body.get('name')
tarfile = body.get('tarfile')
try:
objects.kube_app.get_by_name(pecan.request.context, name)
raise wsme.exc.ClientSideError(_(
"Application-upload rejected: application {} already exists.".format(
name)))
except exception.KubeAppNotFound:
pass
if not cutils.is_url(tarfile):
mname, mfile = self._check_tarfile(name, tarfile)
else:
# For tarfile that is downloaded remotely, defer the checksum, manifest
# and tarfile content validations to sysinv-conductor as download can
# take some time depending on network traffic, target server and file
# size.
mname = constants.APP_MANIFEST_NAME_PLACEHOLDER
mfile = constants.APP_TARFILE_NAME_PLACEHOLDER
# Create a database entry and make an rpc async request to upload
# the application
app_data = {'name': name,
'manifest_name': mname,
'manifest_file': os.path.basename(mfile),
'status': constants.APP_UPLOAD_IN_PROGRESS}
try:
new_app = pecan.request.dbapi.kube_app_create(app_data)
except exception.SysinvException as e:
LOG.exception(e)
raise
pecan.request.rpcapi.perform_app_upload(pecan.request.context,
new_app, tarfile)
return KubeApp.convert_with_links(new_app)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(KubeApp, wtypes.text, wtypes.text, wtypes.text)
def patch(self, name, directive, values):
"""Install/update the specified application
:param name: application name
:param directive: either 'apply' (fresh install/update) or 'remove'
"""
self._check_environment()
if directive not in ['apply', 'remove']:
raise exception.OperationNotPermitted
try:
db_app = objects.kube_app.get_by_name(pecan.request.context, name)
except exception.KubeAppNotFound:
LOG.error("Received a request to %s app %s which does not exist." %
(directive, name))
raise wsme.exc.ClientSideError(_(
"Application-{} rejected: application not found.".format(directive)))
if directive == 'apply':
if db_app.status == constants.APP_APPLY_IN_PROGRESS:
raise wsme.exc.ClientSideError(_(
"Application-apply rejected: install/update is already "
"in progress."))
elif db_app.status not in [constants.APP_UPLOAD_SUCCESS,
constants.APP_APPLY_FAILURE,
constants.APP_APPLY_SUCCESS]:
raise wsme.exc.ClientSideError(_(
"Application-apply rejected: operation is not allowed "
"while the current status is {}.".format(db_app.status)))
db_app.status = constants.APP_APPLY_IN_PROGRESS
db_app.progress = None
db_app.save()
pecan.request.rpcapi.perform_app_apply(pecan.request.context,
db_app)
return KubeApp.convert_with_links(db_app)
else:
if db_app.status not in [constants.APP_APPLY_SUCCESS,
constants.APP_APPLY_FAILURE,
constants.APP_REMOVE_FAILURE]:
raise wsme.exc.ClientSideError(_(
"Application-remove rejected: operation is not allowed while "
"the current status is {}.".format(db_app.status)))
db_app.status = constants.APP_REMOVE_IN_PROGRESS
db_app.progress = None
db_app.save()
pecan.request.rpcapi.perform_app_remove(pecan.request.context,
db_app)
return KubeApp.convert_with_links(db_app)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, name):
"""Delete the application with the given name
:param name: application name
"""
self._check_environment()
try:
db_app = objects.kube_app.get_by_name(pecan.request.context, name)
except exception.KubeAppNotFound:
LOG.error("Received a request to delete app %s which does not "
"exist." % name)
raise
response = pecan.request.rpcapi.perform_app_delete(
pecan.request.context, db_app)
if response:
raise wsme.exc.ClientSideError(_(
"%s." % response))