Support deploying application with versioned app tarball
This commit updates the upload workflow to support uploading versioned application tarball with metadata file. The app name and version are extracted from the metadata file. If the tarball doesn't contain the app name and version, they need to be specified when uploading. If the application has patch dependencies, the required patches need to be fully applied before uploading. After application upload/delete is done, sysinv reports the patch dependencies to patch controller. Tests conducted: - upload/apply/remove/delete(stx-openstack and custom app) - upload via url - failure tests: - metadata file has empty name/version - no app name/version provided from CLI or metadata file - required patch(es) not applies - upload without specify tarball ... Story: 2005350 Task: 30609 Depends-On: https://review.opendev.org/655494 Change-Id: I096d5ac126efc97f9a0a0f54f1e02323d936281c Signed-off-by: Angie Wang <angie.wang@windriver.com>
This commit is contained in:
parent
63253961a1
commit
bc28061090
|
@ -1,2 +1,2 @@
|
|||
SRC_DIR="cgts-client"
|
||||
TIS_PATCH_VER=63
|
||||
TIS_PATCH_VER=64
|
||||
|
|
|
@ -45,8 +45,8 @@ def _is_url(url_str):
|
|||
def do_application_list(cc, args):
|
||||
"""List all containerized applications"""
|
||||
apps = cc.app.list()
|
||||
labels = ['application', 'manifest name', 'manifest file', 'status', 'progress']
|
||||
fields = ['name', 'manifest_name', 'manifest_file', 'status', 'progress']
|
||||
labels = ['application', 'version', 'manifest name', 'manifest file', 'status', 'progress']
|
||||
fields = ['name', 'app_version', 'manifest_name', 'manifest_file', 'status', 'progress']
|
||||
utils.print_list(apps, fields, labels, sortby=0)
|
||||
|
||||
|
||||
|
@ -61,11 +61,15 @@ def do_application_show(cc, args):
|
|||
raise exc.CommandError('application not found: %s' % args.name)
|
||||
|
||||
|
||||
@utils.arg('name', metavar='<app name>',
|
||||
help='Name of the application')
|
||||
@utils.arg('tarfile', metavar='<tar file>',
|
||||
help='Tarball containing application manifest, helm charts and'
|
||||
' config file')
|
||||
@utils.arg('-n', '--app-name',
|
||||
metavar='<app name>',
|
||||
help='Name of the application')
|
||||
@utils.arg('-v', '--app-version',
|
||||
metavar='<app version>',
|
||||
help='Version of the application')
|
||||
def do_application_upload(cc, args):
|
||||
"""Upload application Helm chart(s) and manifest"""
|
||||
tarfile = args.tarfile
|
||||
|
@ -81,11 +85,15 @@ def do_application_upload(cc, args):
|
|||
"extension. Supported extensions are: .tgz "
|
||||
"and .tar.gz" % tarfile)
|
||||
|
||||
data = {'name': args.name,
|
||||
'tarfile': tarfile}
|
||||
data = {'tarfile': tarfile}
|
||||
if args.app_name:
|
||||
data.update({'name': args.app_name})
|
||||
if args.app_version:
|
||||
data.update({'app_version': args.app_version})
|
||||
|
||||
response = cc.app.upload(data)
|
||||
_print_application_show(response)
|
||||
_print_reminder_msg(args.name)
|
||||
_print_reminder_msg(response.name)
|
||||
|
||||
|
||||
@utils.arg('name', metavar='<app name>',
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
SRC_DIR="sysinv"
|
||||
TIS_PATCH_VER=311
|
||||
TIS_PATCH_VER=312
|
||||
|
|
|
@ -17,6 +17,7 @@ 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 patch_api
|
||||
from sysinv.api.controllers.v1 import types
|
||||
from sysinv.api.controllers.v1 import utils
|
||||
from sysinv.common import constants
|
||||
|
@ -25,6 +26,7 @@ from sysinv.common import utils as cutils
|
|||
from sysinv.openstack.common import log
|
||||
from sysinv.openstack.common.gettextutils import _
|
||||
|
||||
import cgcs_patch.constants as patch_constants
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
@ -50,6 +52,9 @@ class KubeApp(base.APIBase):
|
|||
name = wtypes.text
|
||||
"Represents the name of the application"
|
||||
|
||||
app_version = wtypes.text
|
||||
"Represents the version of the application"
|
||||
|
||||
created_at = wtypes.datetime.datetime
|
||||
"Represents the time the application was uploaded"
|
||||
|
||||
|
@ -79,7 +84,7 @@ class KubeApp(base.APIBase):
|
|||
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',
|
||||
app.unset_fields_except(['name', 'app_version', 'manifest_name',
|
||||
'manifest_file', 'status', 'progress'])
|
||||
|
||||
# skip the id
|
||||
|
@ -118,76 +123,45 @@ class KubeAppController(rest.RestController):
|
|||
if not utils.is_kubernetes_config():
|
||||
raise exception.OperationNotPermitted
|
||||
|
||||
def _check_tarfile(self, app_name, app_tarfile):
|
||||
if app_name and app_tarfile:
|
||||
def _check_tarfile(self, app_tarfile, app_name, app_version):
|
||||
def _handle_upload_failure(reason):
|
||||
raise wsme.exc.ClientSideError(_(
|
||||
"Application-upload rejected: " + reason))
|
||||
|
||||
if 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)))
|
||||
_handle_upload_failure(
|
||||
"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)))
|
||||
_handle_upload_failure(
|
||||
"{} 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))))
|
||||
_handle_upload_failure(
|
||||
"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."))
|
||||
_handle_upload_failure("checksum validation failed.")
|
||||
|
||||
mname, mfile = self._find_manifest_file(app_path)
|
||||
app_helper = KubeAppHelper(pecan.request.dbapi)
|
||||
try:
|
||||
name, version, patches = app_helper._verify_metadata_file(
|
||||
app_path, app_name, app_version)
|
||||
mname, mfile = app_helper._find_manifest_file(app_path)
|
||||
app_helper._extract_helm_charts(app_path)
|
||||
LOG.info("Tar file of application %s verified." % name)
|
||||
except exception.SysinvException as e:
|
||||
_handle_upload_failure(str(e))
|
||||
|
||||
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
|
||||
return name, version, 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."))
|
||||
"Application-upload rejected: tar file must be specified."))
|
||||
|
||||
def _get_one(self, app_name):
|
||||
# can result in KubeAppNotFound
|
||||
|
@ -213,8 +187,24 @@ class KubeAppController(rest.RestController):
|
|||
"""Uploading an application to be deployed by Armada"""
|
||||
|
||||
self._check_environment()
|
||||
name = body.get('name')
|
||||
|
||||
tarfile = body.get('tarfile')
|
||||
name = body.get('name', '')
|
||||
version = body.get('app_version', '')
|
||||
|
||||
if not cutils.is_url(tarfile):
|
||||
name, version, mname, mfile = self._check_tarfile(tarfile, name, version)
|
||||
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
|
||||
if not name:
|
||||
name = constants.APP_NAME_PLACEHOLDER
|
||||
if not version:
|
||||
version = constants.APP_VERSION_PLACEHOLDER
|
||||
|
||||
try:
|
||||
objects.kube_app.get_by_name(pecan.request.context, name)
|
||||
|
@ -224,19 +214,10 @@ class KubeAppController(rest.RestController):
|
|||
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,
|
||||
'app_version': version,
|
||||
'manifest_name': mname,
|
||||
'manifest_file': os.path.basename(mfile),
|
||||
'status': constants.APP_UPLOAD_IN_PROGRESS}
|
||||
|
@ -324,3 +305,141 @@ class KubeAppController(rest.RestController):
|
|||
if response:
|
||||
raise wsme.exc.ClientSideError(_(
|
||||
"%s." % response))
|
||||
|
||||
|
||||
class KubeAppHelper(object):
|
||||
|
||||
def __init__(self, dbapi):
|
||||
self._dbapi = dbapi
|
||||
|
||||
def _check_patching_operation(self):
|
||||
try:
|
||||
system = self._dbapi.isystem_get_one()
|
||||
response = patch_api.patch_query(
|
||||
token=None,
|
||||
timeout=constants.PATCH_DEFAULT_TIMEOUT_IN_SECS,
|
||||
region_name=system.region_name
|
||||
)
|
||||
query_patches = response['pd']
|
||||
except Exception as e:
|
||||
LOG.error(_("No response from patch api: %s" % e))
|
||||
return
|
||||
|
||||
for patch in query_patches:
|
||||
patch_state = query_patches[patch].get('patchstate', None)
|
||||
if (patch_state == patch_constants.PARTIAL_APPLY or
|
||||
patch_state == patch_constants.PARTIAL_REMOVE):
|
||||
raise exception.SysinvException(_(
|
||||
"Patching operation is in progress."))
|
||||
|
||||
def _check_patch_is_applied(self, patches):
|
||||
try:
|
||||
system = self._dbapi.isystem_get_one()
|
||||
response = patch_api.patch_is_applied(
|
||||
token=None,
|
||||
timeout=constants.PATCH_DEFAULT_TIMEOUT_IN_SECS,
|
||||
region_name=system.region_name,
|
||||
patches=patches
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
raise exception.SysinvException(_(
|
||||
"Error while querying patch-controller for the "
|
||||
"state of the patch(es)."))
|
||||
return response
|
||||
|
||||
def _patch_report_app_dependencies(self, name, patches=[]):
|
||||
try:
|
||||
system = self._dbapi.isystem_get_one()
|
||||
patch_api.patch_report_app_dependencies(
|
||||
token=None,
|
||||
timeout=constants.PATCH_DEFAULT_TIMEOUT_IN_SECS,
|
||||
region_name=system.region_name,
|
||||
patches=patches,
|
||||
app_name=name
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
raise exception.SysinvException(
|
||||
"Error while reporting the patch dependencies "
|
||||
"to patch-controller.")
|
||||
|
||||
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 exception.SysinvException(_(
|
||||
"manifest file is corrupted."))
|
||||
|
||||
if mfiles:
|
||||
if len(mfiles) == 1:
|
||||
return mfiles[0]
|
||||
else:
|
||||
raise exception.SysinvException(_(
|
||||
"Application-upload rejected: tar file contains more "
|
||||
"than one manifest file."))
|
||||
else:
|
||||
raise exception.SysinvException(_(
|
||||
"Application-upload rejected: manifest file is missing."))
|
||||
|
||||
def _verify_metadata_file(self, app_path, app_name, app_version):
|
||||
try:
|
||||
name, version, patches = cutils.find_metadata_file(
|
||||
app_path, constants.APP_METADATA_FILE)
|
||||
except exception.SysinvException as e:
|
||||
raise exception.SysinvException(_(
|
||||
"metadata validation failed. {}".format(e)))
|
||||
|
||||
if not name:
|
||||
name = app_name
|
||||
if not version:
|
||||
version = app_version
|
||||
|
||||
if (not name or not version or
|
||||
name == constants.APP_VERSION_PLACEHOLDER or
|
||||
version == constants.APP_VERSION_PLACEHOLDER):
|
||||
raise exception.SysinvException(_(
|
||||
"application name or/and version is/are not included "
|
||||
"in the tar file. Please specify the application name "
|
||||
"via --app-name or/and version via --app-version."))
|
||||
|
||||
if patches:
|
||||
try:
|
||||
self._check_patching_operation()
|
||||
except exception.SysinvException as e:
|
||||
raise exception.SysinvException(_(
|
||||
"{}. Please upload after the patching operation "
|
||||
"is completed.".format(e)))
|
||||
|
||||
applied = self._check_patch_is_applied(patches)
|
||||
if not applied:
|
||||
raise exception.SysinvException(_(
|
||||
"the required patch(es) for application {} ({}) "
|
||||
"must be applied".format(name, version)))
|
||||
|
||||
LOG.info("The required patch(es) for application {} ({}) "
|
||||
"has/have applied.".format(name, version))
|
||||
else:
|
||||
LOG.info("No patch required for application {} ({}).".format(name, version))
|
||||
|
||||
return name, version, patches
|
||||
|
||||
def _extract_helm_charts(self, app_path, demote_user=False):
|
||||
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 exception.SysinvException(_(
|
||||
"tar file contains no Helm charts."))
|
||||
if not tar_filelist:
|
||||
raise exception.SysinvException(_(
|
||||
"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), demote_user):
|
||||
raise exception.SysinvException(_(
|
||||
"failed to extract tar file {}.".format(os.path.basename(f))))
|
||||
|
|
|
@ -62,3 +62,45 @@ def patch_drop_host(token, timeout, hostname, region_name):
|
|||
|
||||
response = rest_api_request(token, "POST", api_cmd, timeout=timeout)
|
||||
return response
|
||||
|
||||
|
||||
def patch_is_applied(token, timeout, region_name, patches):
|
||||
"""
|
||||
Query the applied state for a list of patches
|
||||
"""
|
||||
api_cmd = None
|
||||
|
||||
if not token:
|
||||
token = get_token(region_name)
|
||||
if token:
|
||||
api_cmd = token.get_service_url("patching", "patching")
|
||||
|
||||
patch_dependencies = ""
|
||||
for patch in patches:
|
||||
patch_dependencies += "/%s" % patch
|
||||
|
||||
api_cmd += "/v1/is_applied%s" % patch_dependencies
|
||||
|
||||
response = rest_api_request(token, "GET", api_cmd, timeout=timeout)
|
||||
return response
|
||||
|
||||
|
||||
def patch_report_app_dependencies(token, timeout, region_name, patches, app_name):
|
||||
"""
|
||||
Report the application patch dependencies
|
||||
"""
|
||||
api_cmd = None
|
||||
|
||||
if not token:
|
||||
token = get_token(region_name)
|
||||
if token:
|
||||
api_cmd = token.get_service_url("patching", "patching")
|
||||
|
||||
patch_dependencies = ""
|
||||
for patch in patches:
|
||||
patch_dependencies += "/%s" % patch
|
||||
|
||||
api_cmd += "/v1/report_app_dependencies%s?app=%s" % (patch_dependencies, app_name)
|
||||
|
||||
response = rest_api_request(token, "POST", api_cmd, timeout=timeout)
|
||||
return response
|
||||
|
|
|
@ -1484,6 +1484,7 @@ K8S_RBD_PROV_STOR_CLASS_NAME = 'general'
|
|||
APP_INSTALL_ROOT_PATH = '/scratch'
|
||||
APP_INSTALL_PATH = APP_INSTALL_ROOT_PATH + '/apps'
|
||||
APP_SYNCED_DATA_PATH = os.path.join(tsc.PLATFORM_PATH, 'armada', tsc.SW_VERSION)
|
||||
APP_METADATA_FILE = 'metadata.yaml'
|
||||
|
||||
# State constants
|
||||
APP_UPLOAD_IN_PROGRESS = 'uploading'
|
||||
|
@ -1517,6 +1518,8 @@ LABEL_ASSIGN_OP = 'assign'
|
|||
LABEL_REMOVE_OP = 'remove'
|
||||
|
||||
# Placeholder constants
|
||||
APP_NAME_PLACEHOLDER = 'app-name-placeholder'
|
||||
APP_VERSION_PLACEHOLDER = 'app-version-placeholder'
|
||||
APP_MANIFEST_NAME_PLACEHOLDER = 'manifest-placeholder'
|
||||
APP_TARFILE_NAME_PLACEHOLDER = 'tarfile-placeholder'
|
||||
|
||||
|
|
|
@ -225,15 +225,15 @@ class CephPoolSetParamFailure(CephFailure):
|
|||
|
||||
|
||||
class KubeAppUploadFailure(SysinvException):
|
||||
message = _("Upload of application %(name)s failed: %(reason)s")
|
||||
message = _("Upload of application %(name)s (%(version)s) failed: %(reason)s")
|
||||
|
||||
|
||||
class KubeAppApplyFailure(SysinvException):
|
||||
message = _("Deployment of application %(name)s failed: %(reason)s")
|
||||
message = _("Deployment of application %(name)s (%(version)s) failed: %(reason)s")
|
||||
|
||||
|
||||
class KubeAppDeleteFailure(SysinvException):
|
||||
message = _("Delete of application %(name)s failed: %(reason)s")
|
||||
message = _("Delete of application %(name)s (%(version)s) failed: %(reason)s")
|
||||
|
||||
|
||||
class InvalidCPUInfo(Invalid):
|
||||
|
|
|
@ -1896,6 +1896,46 @@ def verify_checksum(path):
|
|||
return rc
|
||||
|
||||
|
||||
def find_metadata_file(path, metadata_file):
|
||||
""" Find and validate the metadata file in a given directory.
|
||||
|
||||
Valid keys for metadata file are defined in the following format:
|
||||
|
||||
app_name: <name>
|
||||
app_version: <version>
|
||||
patch_dependencies:
|
||||
- <patch.1>
|
||||
- <patch.2>
|
||||
...
|
||||
"""
|
||||
app_name = ''
|
||||
app_version = ''
|
||||
patches = []
|
||||
metadata_path = os.path.join(path, metadata_file)
|
||||
if os.path.isfile(metadata_path):
|
||||
with open(metadata_path, 'r') as f:
|
||||
try:
|
||||
doc = yaml.safe_load(f)
|
||||
app_name = doc['app_name']
|
||||
app_version = doc['app_version']
|
||||
patches = doc['patch_dependencies']
|
||||
except KeyError:
|
||||
# metadata file does not have the key(s)
|
||||
pass
|
||||
|
||||
if (app_name is None or
|
||||
app_version is None):
|
||||
raise exception.SysinvException(_(
|
||||
"Invalid %s: app_name or/and app_version "
|
||||
"is/are None." % metadata_file))
|
||||
|
||||
if not isinstance(patches, list):
|
||||
raise exception.SysinvException(_(
|
||||
"Invalid %s: patch_dependencies should "
|
||||
"be a list." % metadata_file))
|
||||
return app_name, app_version, patches
|
||||
|
||||
|
||||
def find_manifest_file(path):
|
||||
""" Find all manifest files in a given directory. """
|
||||
def _is_manifest(yaml_file):
|
||||
|
|
|
@ -29,6 +29,7 @@ from eventlet import queue
|
|||
from eventlet import Timeout
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from sysinv.api.controllers.v1 import kube_app
|
||||
from sysinv.common import constants
|
||||
from sysinv.common import exception
|
||||
from sysinv.common import kubernetes
|
||||
|
@ -125,6 +126,7 @@ class AppOperator(object):
|
|||
self._docker = DockerHelper(self._dbapi)
|
||||
self._helm = helm.HelmOperator(self._dbapi)
|
||||
self._kube = kubernetes.KubeOperator(self._dbapi)
|
||||
self._app = kube_app.KubeAppHelper(self._dbapi)
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _cleanup(self, app):
|
||||
|
@ -173,11 +175,12 @@ class AppOperator(object):
|
|||
from six.moves.urllib.error import HTTPError
|
||||
from six.moves.urllib.error import URLError
|
||||
from socket import timeout as socket_timeout
|
||||
from six.moves.urllib.parse import urlparse
|
||||
from six.moves.urllib.parse import urlsplit
|
||||
|
||||
def _handle_download_failure(reason):
|
||||
raise exception.KubeAppUploadFailure(
|
||||
name=app.name,
|
||||
version=app.version,
|
||||
reason=reason)
|
||||
|
||||
try:
|
||||
|
@ -187,7 +190,7 @@ class AppOperator(object):
|
|||
remote_filename = remote_file.info()['Content-Disposition']
|
||||
except KeyError:
|
||||
remote_filename = os.path.basename(
|
||||
urlparse.urlsplit(remote_file.url).path)
|
||||
urlsplit(remote_file.url).path)
|
||||
|
||||
filename_avail = True if (remote_filename is None or
|
||||
remote_filename == '') else False
|
||||
|
@ -233,23 +236,9 @@ class AppOperator(object):
|
|||
reason='failed to extract tarfile content.'):
|
||||
raise exception.KubeAppUploadFailure(
|
||||
name=app.name,
|
||||
version=app.version,
|
||||
reason=reason)
|
||||
|
||||
def _find_manifest_file(app_path):
|
||||
mfiles = cutils.find_manifest_file(app_path)
|
||||
|
||||
if mfiles is None:
|
||||
_handle_extract_failure('manifest file is corrupted.')
|
||||
|
||||
if mfiles:
|
||||
if len(mfiles) == 1:
|
||||
return mfiles[0]
|
||||
else:
|
||||
_handle_extract_failure(
|
||||
'tarfile contains more than one manifest file.')
|
||||
else:
|
||||
_handle_extract_failure('manifest file is missing.')
|
||||
|
||||
orig_uid, orig_gid = get_app_install_root_path_ownership()
|
||||
|
||||
try:
|
||||
|
@ -269,28 +258,28 @@ class AppOperator(object):
|
|||
_handle_extract_failure()
|
||||
|
||||
if app.downloaded_tarfile:
|
||||
name, version, patches = self._app._verify_metadata_file(
|
||||
app.path, app.name, app.version)
|
||||
if (name != app.name or version != app.version):
|
||||
# Save the official application info. They will be
|
||||
# persisted in the next status update
|
||||
app.regenerate_application_info(name, version, patches)
|
||||
|
||||
if not cutils.verify_checksum(app.path):
|
||||
_handle_extract_failure('checksum validation failed.')
|
||||
mname, mfile = _find_manifest_file(app.path)
|
||||
mname, mfile = self._app._find_manifest_file(app.path)
|
||||
# Save the official manifest file info. They will be persisted
|
||||
# in the next status update
|
||||
app.regenerate_manifest_filename(mname, os.path.basename(mfile))
|
||||
else:
|
||||
name, version, patches = cutils.find_metadata_file(
|
||||
app.path, constants.APP_METADATA_FILE)
|
||||
app.patch_dependencies = patches
|
||||
|
||||
if os.path.isdir(app.charts_dir):
|
||||
if len(os.listdir(app.charts_dir)) == 0:
|
||||
_handle_extract_failure('tarfile contains no Helm charts.')
|
||||
self._app._extract_helm_charts(app.path)
|
||||
|
||||
tar_filelist = cutils.get_files_matching(app.charts_dir,
|
||||
'.tgz')
|
||||
if not tar_filelist:
|
||||
reason = 'tarfile contains no Helm charts of expected ' + \
|
||||
'file extension (.tgz).'
|
||||
_handle_extract_failure(reason)
|
||||
|
||||
for p, f in tar_filelist:
|
||||
if not cutils.extract_tarfile(
|
||||
p, os.path.join(p, f), demote_user=True):
|
||||
_handle_extract_failure()
|
||||
except exception.SysinvException as e:
|
||||
_handle_extract_failure(str(e))
|
||||
except OSError as e:
|
||||
LOG.error(e)
|
||||
_handle_extract_failure()
|
||||
|
@ -441,6 +430,7 @@ class AppOperator(object):
|
|||
"""
|
||||
raise exception.KubeAppApplyFailure(
|
||||
name=app.name,
|
||||
version=app.version,
|
||||
reason="embedded images are not yet supported.")
|
||||
|
||||
def _save_images_list(self, app):
|
||||
|
@ -466,6 +456,7 @@ class AppOperator(object):
|
|||
# an info log and let it advance to the next step.
|
||||
raise exception.KubeAppUploadFailure(
|
||||
name=app.name,
|
||||
version=app.version,
|
||||
reason="charts specify no docker images.")
|
||||
|
||||
with open(app.imgfile_abs, 'ab') as f:
|
||||
|
@ -536,6 +527,7 @@ class AppOperator(object):
|
|||
if failed_count > 0:
|
||||
raise exception.KubeAppApplyFailure(
|
||||
name=app.name,
|
||||
version=app.version,
|
||||
reason="failed to download one or more image(s).")
|
||||
else:
|
||||
LOG.info("All docker images for application %s were successfully "
|
||||
|
@ -557,11 +549,11 @@ class AppOperator(object):
|
|||
failed_charts.append(r)
|
||||
except Exception as e:
|
||||
raise exception.KubeAppUploadFailure(
|
||||
name=app.name, reason=str(e))
|
||||
name=app.name, version=app.version, reason=str(e))
|
||||
|
||||
if len(failed_charts) > 0:
|
||||
raise exception.KubeAppUploadFailure(
|
||||
name=app.name, reason="one or more charts failed validation.")
|
||||
name=app.name, version=app.version, reason="one or more charts failed validation.")
|
||||
|
||||
def _upload_helm_charts(self, app):
|
||||
# Set env path for helm-upload execution
|
||||
|
@ -582,7 +574,7 @@ class AppOperator(object):
|
|||
LOG.info("Helm chart %s uploaded" % os.path.basename(chart))
|
||||
except Exception as e:
|
||||
raise exception.KubeAppUploadFailure(
|
||||
name=app.name, reason=str(e))
|
||||
name=app.name, version=app.version, reason=str(e))
|
||||
finally:
|
||||
os.chown(constants.APP_INSTALL_ROOT_PATH, orig_uid, orig_gid)
|
||||
|
||||
|
@ -949,6 +941,9 @@ class AppOperator(object):
|
|||
|
||||
self._save_images_list(app)
|
||||
self._update_app_status(app, constants.APP_UPLOAD_SUCCESS)
|
||||
if app.patch_dependencies:
|
||||
self._app._patch_report_app_dependencies(
|
||||
app.name, app.patch_dependencies)
|
||||
LOG.info("Application (%s) upload completed." % app.name)
|
||||
except exception.KubeAppUploadFailure as e:
|
||||
LOG.exception(e)
|
||||
|
@ -1080,6 +1075,7 @@ class AppOperator(object):
|
|||
try:
|
||||
self._dbapi.kube_app_destroy(app.name)
|
||||
self._cleanup(app)
|
||||
self._app._patch_report_app_dependencies(app.name)
|
||||
LOG.info("Application (%s) has been purged from the system." %
|
||||
app.name)
|
||||
msg = None
|
||||
|
@ -1118,12 +1114,17 @@ class AppOperator(object):
|
|||
self.imgfile_abs = generate_images_filename_abs(
|
||||
self._kube_app.get('name'))
|
||||
|
||||
self.patch_dependencies = []
|
||||
self.charts = []
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._kube_app.get('name')
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._kube_app.get('app_version')
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._kube_app.get('status')
|
||||
|
@ -1148,6 +1149,21 @@ class AppOperator(object):
|
|||
self.mfile_abs = generate_manifest_filename_abs(
|
||||
self.name, new_mfile)
|
||||
|
||||
def regenerate_application_info(self, new_name, new_version, new_patch_dependencies):
|
||||
self._kube_app.name = new_name
|
||||
self._kube_app.app_version = new_version
|
||||
self.system_app = \
|
||||
(self.name == constants.HELM_APP_OPENSTACK)
|
||||
self.imgfile_abs = \
|
||||
generate_images_filename_abs(self.name)
|
||||
new_path = os.path.join(
|
||||
constants.APP_INSTALL_PATH, self.name)
|
||||
os.rename(self.path, new_path)
|
||||
self.path = new_path
|
||||
self.charts_dir = os.path.join(self.path, 'charts')
|
||||
self.images_dir = os.path.join(self.path, 'images')
|
||||
self.patch_dependencies = new_patch_dependencies
|
||||
|
||||
|
||||
class DockerHelper(object):
|
||||
""" Utility class to encapsulate Docker related operations """
|
||||
|
|
|
@ -7566,14 +7566,14 @@ class Connection(api.Connection):
|
|||
return self._kube_app_get(name)
|
||||
|
||||
@objects.objectify(objects.kube_app)
|
||||
def kube_app_update(self, name, values):
|
||||
def kube_app_update(self, app_id, values):
|
||||
with _session_for_write() as session:
|
||||
query = model_query(models.KubeApp, session=session)
|
||||
query = query.filter_by(name=name)
|
||||
query = query.filter_by(id=app_id)
|
||||
|
||||
count = query.update(values, synchronize_session='fetch')
|
||||
if count == 0:
|
||||
raise exception.KubeAppNotFound(name)
|
||||
raise exception.KubeAppNotFound(id)
|
||||
return query.one()
|
||||
|
||||
def kube_app_destroy(self, name):
|
||||
|
|
|
@ -33,6 +33,7 @@ def upgrade(migrate_engine):
|
|||
Column('updated_at', DateTime),
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('name', String(255), unique=True, nullable=False),
|
||||
Column('app_version', String(255), nullable=False),
|
||||
Column('manifest_name', String(255), nullable=False),
|
||||
Column('manifest_file', String(255), nullable=True),
|
||||
Column('status', String(255), nullable=False),
|
||||
|
|
|
@ -1696,6 +1696,7 @@ class KubeApp(Base):
|
|||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(255), unique=True, nullable=False)
|
||||
app_version = Column(String(255), nullable=False)
|
||||
manifest_name = Column(String(255), nullable=False)
|
||||
manifest_file = Column(String(255), nullable=False)
|
||||
status = Column(String(255), nullable=False)
|
||||
|
|
|
@ -17,7 +17,9 @@ class KubeApp(base.SysinvObject):
|
|||
|
||||
dbapi = db_api.get_instance()
|
||||
|
||||
fields = {'name': utils.str_or_none,
|
||||
fields = {'id': int,
|
||||
'name': utils.str_or_none,
|
||||
'app_version': utils.str_or_none,
|
||||
'manifest_name': utils.str_or_none,
|
||||
'manifest_file': utils.str_or_none,
|
||||
'status': utils.str_or_none,
|
||||
|
@ -29,4 +31,4 @@ class KubeApp(base.SysinvObject):
|
|||
return cls.dbapi.kube_app_get(name)
|
||||
|
||||
def save_changes(self, context, updates):
|
||||
self.dbapi.kube_app_update(self.name, updates)
|
||||
self.dbapi.kube_app_update(self.id, updates)
|
||||
|
|
Loading…
Reference in New Issue