Merge "Support deploying application with versioned app tarball"

This commit is contained in:
Zuul 2019-05-01 20:57:51 +00:00 committed by Gerrit Code Review
commit 0e5b63bc57
13 changed files with 352 additions and 120 deletions

View File

@ -1,2 +1,2 @@
SRC_DIR="cgts-client"
TIS_PATCH_VER=63
TIS_PATCH_VER=64

View File

@ -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>',

View File

@ -1,2 +1,2 @@
SRC_DIR="sysinv"
TIS_PATCH_VER=311
TIS_PATCH_VER=312

View File

@ -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))))

View File

@ -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

View File

@ -1481,6 +1481,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'
@ -1514,6 +1515,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'

View File

@ -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):

View File

@ -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):

View 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 """

View File

@ -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):

View File

@ -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),

View File

@ -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)

View File

@ -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)