Robustness updates to support application plugins

Changes include:
- Supporting viewing overrides for non-applied uploaded only
  applications
- Re-enable sysinv-helm commands to support plugin development
- Expand version pattern match when discovering plugins to account for
  tarball versions produced from the Cengn build
- Ensure that applications which are in an apply related state have
  their plugins enabled when the conductor is restarted.

Change-Id: Ic233ec529065de80105a077d83e56afda2f3ee14
Closes-Bug: #1881711
Closes-Bug: #1881454
Signed-off-by: Robert Church <robert.church@windriver.com>
This commit is contained in:
Robert Church 2020-06-03 13:52:57 -04:00
parent 7149c60059
commit 4617f4d0be
7 changed files with 134 additions and 57 deletions

View File

@ -77,13 +77,12 @@ class HelmChartsController(rest.RestController):
except exception.HelmOverrideNotFound:
user_overrides = None
system_apps = pecan.request.rpcapi.get_helm_applications(
pecan.request.context)
if app_name in system_apps:
if pecan.request.rpcapi.app_has_system_plugins(pecan.request.context,
app_name):
# Get any system overrides for system app.
try:
system_overrides = pecan.request.rpcapi.get_helm_chart_overrides(
pecan.request.context, name, namespace)
pecan.request.context, app_name, name, namespace)
system_overrides = yaml.safe_dump(system_overrides) \
if system_overrides else None
except Exception as e:

View File

@ -14,7 +14,10 @@ import sys
from oslo_config import cfg
from oslo_log import log
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common import service
from sysinv.conductor import kube_app
from sysinv.db import api
from sysinv.helm import helm
@ -25,35 +28,56 @@ LOG = log.getLogger(__name__)
def create_app_overrides_action(path, app_name=None, namespace=None):
dbapi = api.get_instance()
operator = helm.HelmOperator(dbapi=dbapi)
system_apps = operator.get_helm_applications()
if app_name not in system_apps:
try:
db_app = dbapi.kube_app_get(app_name)
except exception.KubeAppNotFound:
LOG.info("Application %s not found" % app_name)
return
helm_operator = helm.HelmOperator(dbapi=dbapi)
app_operator = kube_app.AppOperator(dbapi, helm_operator)
if not app_operator.app_has_system_plugins(db_app):
LOG.info("Overrides generation for application %s is "
"not supported via this command." % app_name)
else:
operator.generate_helm_application_overrides(path, app_name, mode=None,
cnamespace=namespace)
if db_app.status == constants.APP_UPLOAD_SUCCESS:
app_operator.activate_app_plugins(db_app)
helm_operator.generate_helm_application_overrides(
path, app_name, mode=None, cnamespace=namespace)
app_operator.deactivate_app_plugins(db_app)
else:
helm_operator.generate_helm_application_overrides(
path, app_name, mode=None, cnamespace=namespace)
def create_armada_app_overrides_action(path, app_name=None, namespace=None):
dbapi = api.get_instance()
operator = helm.HelmOperator(dbapi=dbapi)
system_apps = operator.get_helm_applications()
if app_name not in system_apps:
try:
db_app = dbapi.kube_app_get(app_name)
except exception.KubeAppNotFound:
LOG.info("Application %s not found" % app_name)
return
helm_operator = helm.HelmOperator(dbapi=dbapi)
app_operator = kube_app.AppOperator(dbapi, helm_operator)
if not app_operator.app_has_system_plugins(db_app):
LOG.info("Overrides generation for application %s is "
"not supported via this command." % app_name)
else:
operator.generate_helm_application_overrides(path, app_name, mode=None,
cnamespace=namespace,
armada_format=True,
armada_chart_info=None,
combined=False)
def create_chart_override_action(path, chart_name=None, namespace=None):
dbapi = api.get_instance()
operator = helm.HelmOperator(dbapi=dbapi)
operator.generate_helm_chart_overrides(path, chart_name, namespace)
if db_app.status == constants.APP_UPLOAD_SUCCESS:
app_operator.activate_app_plugins(db_app)
helm_operator.generate_helm_application_overrides(
path, app_name, mode=None, cnamespace=namespace,
armada_format=True, armada_chart_info=None, combined=False)
app_operator.deactivate_app_plugins(db_app)
else:
helm_operator.generate_helm_application_overrides(
path, app_name, mode=None, cnamespace=namespace,
armada_format=True, armada_chart_info=None, combined=False)
def add_action_parsers(subparsers):
@ -69,12 +93,6 @@ def add_action_parsers(subparsers):
parser.add_argument('app_name', nargs='?')
parser.add_argument('namespace', nargs='?')
parser = subparsers.add_parser('create-chart-overrides')
parser.set_defaults(func=create_chart_override_action)
parser.add_argument('path', nargs='?')
parser.add_argument('chart_name', nargs='?')
parser.add_argument('namespace', nargs='?')
CONF.register_cli_opt(
cfg.SubCommandOpt('action',
@ -103,10 +121,3 @@ def main():
CONF.action.func(CONF.action.path,
CONF.action.app_name,
CONF.action.namespace)
elif CONF.action.name == 'create-chart-overrides':
try:
CONF.action.func(CONF.action.path,
CONF.action.chart_name,
CONF.action.namespace)
except Exception as e:
print(e)

View File

@ -160,6 +160,21 @@ class AppOperator(object):
# applications
self._plugins.audit_plugins()
def activate_app_plugins(self, rpc_app):
app = AppOperator.Application(rpc_app)
self._plugins.activate_plugins(app)
def deactivate_app_plugins(self, rpc_app):
app = AppOperator.Application(rpc_app)
self._plugins.deactivate_plugins(app)
def app_has_system_plugins(self, rpc_app):
app = AppOperator.Application(rpc_app)
# TODO(rchurch): Update once apps are decoupled
return (app.system_app or
app.name == constants.HELM_APP_CERT_MANAGER or
app.name == constants.HELM_APP_OIDC_AUTH)
def _clear_armada_locks(self):
lock_name = "{}.{}.{}".format(ARMADA_LOCK_PLURAL,
ARMADA_LOCK_GROUP,
@ -3228,7 +3243,8 @@ class PluginHelper(object):
# An enabled plugin will have a python path configuration file name with the
# following format: stx_app-platform-integ-apps-1.0-8.pth
PTH_PREFIX = 'stx_app-'
PTH_PATTERN = re.compile("{}/([\w-]+)/(\d+\.\d+-\d+)".format(common.HELM_OVERRIDES_PATH))
PTH_PATTERN = re.compile("{}/([\w-]+)/(\d+\.\d+-\d+.*)/plugins".format(
common.HELM_OVERRIDES_PATH))
def __init__(self, dbapi, helm_op):
self._dbapi = dbapi
@ -3267,6 +3283,7 @@ class PluginHelper(object):
discoverable_pths = glob.glob(pattern)
LOG.debug("PluginHelper: Discoverable app plugins: %s" % discoverable_pths)
# Examine existing pth files to make sure they are still valid
for pth in discoverable_pths:
with open(pth, 'r') as f:
contents = f.readlines()
@ -3286,7 +3303,8 @@ class PluginHelper(object):
else:
LOG.warning("PluginHelper: Stale plugin pth file "
"found %s: Wrong plugin version "
"enabled %s." % (pth, ver))
"enabled %s != %s." % (
pth, ver, app_obj.app_version))
except exception.KubeAppNotFound:
LOG.warning("PluginHelper: Stale plugin pth file found"
" %s: App is not active." % pth)
@ -3300,6 +3318,17 @@ class PluginHelper(object):
LOG.info("PluginHelper: Removing invalid plugin pth: %s" % pth)
os.remove(pth)
# Examine existing applications in an applying state and make sure they
# are activated
apps = self._dbapi.kube_app_get_all()
for app in apps:
# If the app is in some form of apply the the plugins should be
# enabled
if app.status in [constants.APP_APPLY_IN_PROGRESS,
constants.APP_APPLY_SUCCESS,
constants.APP_APPLY_FAILURE]:
self.activate_plugins(AppOperator.Application(app))
def install_plugins(self, app):
""" Install application plugins. """
@ -3340,9 +3369,15 @@ class PluginHelper(object):
"need to remove." % app.sync_plugins_dir)
def activate_plugins(self, app):
pth_fqpn = self._get_pth_fqpn(app)
# If this isn't an app with plugins or the plugin path is already
# active, skip activation
if not app.system_app or os.path.isfile(pth_fqpn):
return
# Add a .pth file to a site-packages directory so the plugin is picked
# automatically on a conductor restart
pth_fqpn = self._get_pth_fqpn(app)
with open(pth_fqpn, 'w') as f:
f.write(app.sync_plugins_dir + '\n')
LOG.info("PluginHelper: Enabled plugin directory %s: created %s" % (
@ -3362,6 +3397,10 @@ class PluginHelper(object):
self._helm_op.discover_plugins()
def deactivate_plugins(self, app):
# If the application doesn't have any plugins, skip deactivation
if not app.system_app:
return
pth_fqpn = self._get_pth_fqpn(app)
if os.path.exists(pth_fqpn):
# Remove the pth file, so on a conductor restart this installed

View File

@ -10757,7 +10757,8 @@ class ConductorManager(service.PeriodicService):
"""
return self._helm.get_helm_chart_namespaces(chart_name)
def get_helm_chart_overrides(self, context, chart_name, cnamespace=None):
def get_helm_chart_overrides(self, context, app_name, chart_name,
cnamespace=None):
"""Get the overrides for a supported chart.
This method retrieves overrides for a supported chart. Overrides for
@ -10765,6 +10766,7 @@ class ConductorManager(service.PeriodicService):
is requested.
:param context: request context.
:param app_name: name of a supported application
:param chart_name: name of a supported chart
:param cnamespace: (optional) namespace
:returns: dict of overrides.
@ -10794,16 +10796,30 @@ class ConductorManager(service.PeriodicService):
}
}
"""
return self._helm.get_helm_chart_overrides(chart_name,
cnamespace)
def get_helm_applications(self, context):
"""Get supported applications.
app = kubeapp_obj.get_by_name(context, app_name)
if app.status in [constants.APP_APPLY_IN_PROGRESS,
constants.APP_APPLY_SUCCESS,
constants.APP_APPLY_FAILURE]:
overrides = self._helm.get_helm_chart_overrides(chart_name,
cnamespace)
else:
self._app.activate_app_plugins(app)
overrides = self._helm.get_helm_chart_overrides(chart_name,
cnamespace)
self._app.deactivate_app_plugins(app)
:returns: a list of suppotred applications that associated overrides may
be provided.
return overrides
def app_has_system_plugins(self, context, app_name):
"""Determine if the application has system plugin support.
:returns: True if the application has system plugins and can generate
system overrides.
"""
return self._helm.get_helm_applications()
app = kubeapp_obj.get_by_name(context, app_name)
return self._app.app_has_system_plugins(app)
def get_helm_application_namespaces(self, context, app_name):
"""Get supported application namespaces.

View File

@ -1608,10 +1608,12 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy):
self.make_msg('get_helm_chart_namespaces',
chart_name=chart_name))
def get_helm_chart_overrides(self, context, chart_name, cnamespace=None):
def get_helm_chart_overrides(self, context, app_name, chart_name,
cnamespace=None):
"""Get the overrides for a supported chart.
:param context: request context.
:param app_name: name of a supported application
:param chart_name: name of a supported chart
:param cnamespace: (optional) namespace
:returns: dict of overrides.
@ -1619,18 +1621,20 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy):
"""
return self.call(context,
self.make_msg('get_helm_chart_overrides',
app_name=app_name,
chart_name=chart_name,
cnamespace=cnamespace))
def get_helm_applications(self, context):
def app_has_system_plugins(self, context, app_name):
"""Get supported applications.
"""Determine if the application has system plugin support.
:returns: a list of suppotred applications that associated overrides may
be provided.
:returns: True if the application has system plugins and can generate
system overrides.
"""
return self.call(context,
self.make_msg('get_helm_applications'))
self.make_msg('app_has_system_plugins',
app_name=app_name))
def get_helm_application_namespaces(self, context, app_name):
"""Get supported application namespaces.

View File

@ -222,8 +222,8 @@ class HelmOperator(object):
return supported_helm_applications
def get_helm_applications(self):
""" Get the system applications and charts """
def get_active_helm_applications(self):
""" Get the active system applications and charts """
return self.helm_system_applications
@property

View File

@ -18,8 +18,9 @@ from sysinv.tests.db import utils as dbutils
class FakeConductorAPI(object):
def __init__(self):
self.app_has_system_plugins = mock.MagicMock()
self.get_helm_application_namespaces = mock.MagicMock()
self.get_helm_applications = mock.MagicMock()
self.get_active_helm_applications = mock.MagicMock()
self.get_helm_chart_overrides = mock.MagicMock()
self.merge_overrides = mock.MagicMock()
@ -72,10 +73,11 @@ class ApiHelmChartTestCaseMixin(base.FunctionalTest,
chart_namespace='kube-system',
system_override_attr={"enabled": False},
user_override="global:\n replicas: \"3\"\n")
self.fake_helm_apps = self.fake_conductor_api.get_helm_applications
self.fake_helm_apps = self.fake_conductor_api.get_active_helm_applications
self.fake_ns = self.fake_conductor_api.get_helm_application_namespaces
self.fake_override = self.fake_conductor_api.get_helm_chart_overrides
self.fake_merge_overrides = self.fake_conductor_api.merge_overrides
self.fake_system_app = self.fake_conductor_api.app_has_system_plugins
def exception_helm_override(self):
print('Raised a fake exception')
@ -169,6 +171,7 @@ class ApiHelmChartShowTestSuiteMixin(ApiHelmChartTestCaseMixin):
super(ApiHelmChartShowTestSuiteMixin, self).setUp()
def test_no_system_override(self):
self.fake_system_app.return_value = False
url = self.get_single_url_helm_override('platform-integ-apps',
'ceph-pools-audit', 'kube-system')
response = self.get_json(url)
@ -190,6 +193,8 @@ class ApiHelmChartShowTestSuiteMixin(ApiHelmChartTestCaseMixin):
response.json['error_message'])
def test_fetch_helm_override_show_invalid_helm_chart(self):
self.fake_system_app.return_value = False
url = self.get_single_url_helm_override('platform-integ-apps',
'invalid_value', 'kube-system')
response = self.get_json(url, expect_errors=True)
@ -202,6 +207,7 @@ class ApiHelmChartShowTestSuiteMixin(ApiHelmChartTestCaseMixin):
response.json['error_message'])
def test_fetch_helm_override_show_invalid_namespace(self):
self.fake_system_app.return_value = False
url = self.get_single_url_helm_override('platform-integ-apps',
'ceph-pools-audit',
'invalid_value')
@ -287,6 +293,7 @@ class ApiHelmChartDeleteTestSuiteMixin(ApiHelmChartTestCaseMixin):
# Test that a valid DELETE operation is successful
def test_delete_helm_override_success(self):
self.fake_system_app.return_value = False
# Verify that user override exists initially
url = self.get_single_url_helm_override('platform-integ-apps',
@ -494,6 +501,7 @@ class ApiHelmChartPatchTestSuiteMixin(ApiHelmChartTestCaseMixin):
'set': ['global.replicas=2']}},
headers=self.API_HEADERS,
expect_errors=True)
self.fake_system_app.return_value = False
response = self.get_json(url, expect_errors=True)
self.assertEqual(response.status_code, http_client.OK)
# Verify the values of the response with the values in database