#
# Copyright (c) 2018-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#

""" System Inventory Helm Overrides Operator."""

from __future__ import absolute_import

import eventlet
import os
import tempfile
import yaml

from six import iteritems
from stevedore import extension

from oslo_log import log as logging
from sysinv.common import exception
from sysinv.common import utils
from sysinv.helm import common
from sysinv.helm import utils as helm_utils


LOG = logging.getLogger(__name__)

# Disable yaml feature 'alias' for clean and readable output
yaml.Dumper.ignore_aliases = lambda *data: True

# Number of characters to strip off from helm plugin name defined in setup.cfg,
# in order to allow controlling the order of the helm plugins, without changing
# the names of the plugins.
# The convention here is for the helm plugins to be named ###_PLUGINNAME.
HELM_PLUGIN_PREFIX_LENGTH = 4

# Number of optional characters appended to FluxCD kustomize operator name, to
# allow overriding with a newer version of the FluxCD kustomize operator. The
# convention here is for the FluxCD kustomize operator plugins to allow an
# optional suffix, as in PLUGINNAME_###.
FLUXCD_PLUGIN_SUFFIX_LENGTH = 4

# Number of optional characters appended to AppLifecycle operator name,
# to allow overriding with a newer version of the AppLifecycle operator.
# The convention here is for the AppLifecycle operator plugins to allow an
# optional suffix, as in PLUGINNAME_###.
LIFECYCLE_PLUGIN_SUFFIX_LENGTH = 4


def helm_context(func):
    """Decorate to initialize the local threading context"""

    def _wrapper(self, *args, **kwargs):
        thread_context = eventlet.greenthread.getcurrent()
        setattr(thread_context, '_helm_context', dict())
        return func(self, *args, **kwargs)
    return _wrapper


def suppress_stevedore_errors(manager, entrypoint, exception):
    """
    stevedore.ExtensionManager will try to import the entry point defined in the module.
    For helm_applications, both stx_openstack and platform_integ_apps are virtual modules.
    So ExtensionManager will throw the "Could not load ..." error message, which is expected.
    Just suppress this error message to avoid cause confusion.
    """
    pass


LOCK_NAME = 'HelmOperator'


class HelmOperator(object):
    """Class to encapsulate helm override operations for System Inventory"""

    # Define the stevedore namespaces that will need to be managed for plugins
    STEVEDORE_APPS = 'systemconfig.helm_applications'
    STEVEDORE_FLUXCD = 'systemconfig.fluxcd.kustomize_ops'
    STEVEDORE_LIFECYCLE = 'systemconfig.app_lifecycle'

    def __init__(self, dbapi=None):
        self.dbapi = dbapi

        # Find all plugins for apps, charts per app, and fluxcd operators
        self.discover_plugins()

    @utils.synchronized(LOCK_NAME)
    def discover_plugins(self):
        """ Scan for all available plugins """

        LOG.debug("HelmOperator: Loading available helm, fluxcd and lifecycle plugins.")

        # Initialize the plugins
        self.helm_system_applications = {}
        self.chart_operators = {}
        self.fluxcd_kustomize_operators = {}
        self.app_lifecycle_operators = {}

        # Need to purge the stevedore plugin cache so that when we discover the
        # plugins, new plugin resources are found. If the cache exists, then no
        # new plugins are discoverable.
        self.purge_cache()

        # dict containing sequence of helm charts per app
        self.helm_system_applications = self._load_helm_applications()

        # dict containing FluxCD kustomize operators per app
        self.fluxcd_kustomize_operators = self._load_fluxcd_kustomize_operators()

        # dict containing app lifecycle operators per app
        self.app_lifecycle_operators = self._load_app_lifecycle_operators()

    @utils.synchronized(LOCK_NAME)
    def purge_cache_by_location(self, install_location):
        """Purge the stevedore entry point cache."""
        for lifecycle_ep in extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_LIFECYCLE]:
            lifecycle_distribution = None

            try:
                lifecycle_distribution = utils.get_distribution_from_entry_point(lifecycle_ep)
                (project_name, project_location) = \
                    utils.get_project_name_and_location_from_distribution(lifecycle_distribution)

                if project_location == install_location:
                    extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_LIFECYCLE].remove(lifecycle_ep)
                    break
            except Exception as e:
                LOG.error("Error while trying to purge lifecycle entry point {}, error: {}".
                          format(lifecycle_ep, e))

                # Temporary suppress errors on Debian until Stevedore is reworked.
                # See https://storyboard.openstack.org/#!/story/2009101
                if utils.is_debian():
                    LOG.info("Deleting {} from cache".format(lifecycle_ep))
                    try:
                        extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_LIFECYCLE].remove(lifecycle_ep)
                    except Exception as e:
                        LOG.error("Tried removing lifecycle_ep {}, error: {}".format(lifecycle_ep, e))
                else:
                    raise
        else:
            LOG.info("Couldn't find endpoint distribution located at %s for "
                     "%s" % (install_location, lifecycle_distribution))

        for fluxcd_ep in extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_FLUXCD]:
            fluxcd_distribution = None

            try:
                fluxcd_distribution = utils.get_distribution_from_entry_point(fluxcd_ep)
                (project_name, project_location) = \
                    utils.get_project_name_and_location_from_distribution(fluxcd_distribution)

                if project_location == install_location:
                    extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_FLUXCD].remove(fluxcd_ep)
                    break
            except Exception as e:
                LOG.error("Error while trying to purge flux entry point {}, error: {}".
                          format(fluxcd_ep, e))

                # Temporary suppress errors on Debian until Stevedore is reworked.
                # See https://storyboard.openstack.org/#!/story/2009101
                if utils.is_debian():
                    LOG.info("Deleting {} from cache".format(fluxcd_ep))
                    try:
                        extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_FLUXCD].remove(fluxcd_ep)
                    except Exception as e:
                        LOG.error("Tried removing fluxcd_ep {}, error: {}".format(fluxcd_ep, e))
                else:
                    raise
        else:
            LOG.info("Couldn't find endpoint distribution located at %s for "
                     "%s" % (install_location, fluxcd_distribution))

        for app_ep in extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_APPS]:
            try:
                if utils.is_debian():
                    if app_ep.name in install_location:
                        namespace = app_ep.value
                        purged_list = []
                        for helm_ep in extension.ExtensionManager.ENTRY_POINT_CACHE[namespace]:
                            helm_distribution = utils.get_distribution_from_entry_point(helm_ep)
                            (helm_project_name, helm_project_location) = \
                                utils.get_project_name_and_location_from_distribution(helm_distribution)

                            if helm_project_location != install_location:
                                purged_list.append(helm_ep)

                        if purged_list:
                            extension.ExtensionManager.ENTRY_POINT_CACHE[namespace] = purged_list
                        else:
                            del extension.ExtensionManager.ENTRY_POINT_CACHE[namespace]
                            extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_APPS].remove(app_ep)
                            LOG.info("Removed stevedore namespace: %s" % namespace)
                else:
                    app_distribution = utils.get_distribution_from_entry_point(app_ep)
                    (app_project_name, app_project_location) = \
                        utils.get_project_name_and_location_from_distribution(app_distribution)

                    if app_project_location == install_location:
                        namespace = utils.get_module_name_from_entry_point(app_ep)

                        purged_list = []
                        for helm_ep in extension.ExtensionManager.ENTRY_POINT_CACHE[namespace]:
                            helm_distribution = utils.get_distribution_from_entry_point(helm_ep)
                            (helm_project_name, helm_project_location) = \
                                utils.get_project_name_and_location_from_distribution(helm_distribution)

                            if helm_project_location != install_location:
                                purged_list.append(helm_ep)

                        if purged_list:
                            extension.ExtensionManager.ENTRY_POINT_CACHE[namespace] = purged_list
                        else:
                            del extension.ExtensionManager.ENTRY_POINT_CACHE[namespace]
                            extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_APPS].remove(app_ep)
                            LOG.info("Removed stevedore namespace: %s" % namespace)
            except Exception as e:
                # Temporary suppress errors on Debian until Stevedore is reworked.
                # See https://storyboard.openstack.org/#!/story/2009101
                if utils.is_debian():
                    LOG.info("Tried removing app_ep {}, error: {}".format(app_ep, e))
                    continue
                else:
                    raise

    def purge_cache(self):
        """Purge the stevedore entry point cache."""
        if self.STEVEDORE_APPS in extension.ExtensionManager.ENTRY_POINT_CACHE:
            for entry_point in extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_APPS]:
                namespace = utils.get_module_name_from_entry_point(entry_point)
                try:
                    del extension.ExtensionManager.ENTRY_POINT_CACHE[namespace]
                    LOG.debug("Deleted entry points for %s." % namespace)
                except KeyError:
                    LOG.info("No entry points for %s found." % namespace)

            try:
                del extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_APPS]
                LOG.debug("Deleted entry points for %s." % self.STEVEDORE_APPS)
            except KeyError:
                LOG.info("No entry points for %s found." % self.STEVEDORE_APPS)

        else:
            LOG.info("No entry points for %s found." % self.STEVEDORE_APPS)

        try:
            del extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_FLUXCD]
            LOG.debug("Deleted entry points for %s." % self.STEVEDORE_FLUXCD)
        except KeyError:
            LOG.info("No entry points for %s found." % self.STEVEDORE_FLUXCD)

        try:
            del extension.ExtensionManager.ENTRY_POINT_CACHE[self.STEVEDORE_LIFECYCLE]
            LOG.debug("Deleted entry points for %s." % self.STEVEDORE_LIFECYCLE)
        except KeyError:
            LOG.info("No entry points for %s found." % self.STEVEDORE_LIFECYCLE)

    def _load_app_lifecycle_operators(self):
        """Build a dictionary of AppLifecycle operators"""

        operators_dict = {}

        app_lifecycle_operators = extension.ExtensionManager(
            namespace=self.STEVEDORE_LIFECYCLE,
            invoke_on_load=True, invoke_args=())

        sorted_app_lifecycle_operators = sorted(
            app_lifecycle_operators.extensions, key=lambda x: x.name)

        for operator in sorted_app_lifecycle_operators:
            if (operator.name[-(LIFECYCLE_PLUGIN_SUFFIX_LENGTH - 1):].isdigit() and
                    operator.name[-LIFECYCLE_PLUGIN_SUFFIX_LENGTH:-3] == '_'):
                operator_name = operator.name[0:-LIFECYCLE_PLUGIN_SUFFIX_LENGTH]
            else:
                operator_name = operator.name
            operators_dict[operator_name] = operator.obj

        return operators_dict

    def get_app_lifecycle_operator(self, app_name):
        """Return an AppLifecycle operator based on app name"""

        plugin_name = utils.find_app_plugin_name(app_name)
        if plugin_name in self.app_lifecycle_operators:
            operator = self.app_lifecycle_operators[plugin_name]
        else:
            operator = self.app_lifecycle_operators['generic']

        return operator

    def _load_fluxcd_kustomize_operators(self):
        """Build a dictionary of FluxCD kustomize operators"""

        operators_dict = {}
        dist_info_dict = {}

        fluxcd_kustomize_operators = extension.ExtensionManager(
            namespace=self.STEVEDORE_FLUXCD,
            invoke_on_load=True, invoke_args=())

        sorted_fluxcd_kustomize_operators = sorted(
            fluxcd_kustomize_operators.extensions, key=lambda x: x.name)

        for op in sorted_fluxcd_kustomize_operators:
            if (op.name[-(FLUXCD_PLUGIN_SUFFIX_LENGTH - 1):].isdigit() and
                    op.name[-FLUXCD_PLUGIN_SUFFIX_LENGTH:-3] == '_'):
                op_name = op.name[0:-FLUXCD_PLUGIN_SUFFIX_LENGTH]
            else:
                op_name = op.name
            operators_dict[op_name] = op.obj

            distribution = utils.get_distribution_from_entry_point(op.entry_point)
            (project_name, project_location) = \
                utils.get_project_name_and_location_from_distribution(distribution)

            # Extract distribution information for logging
            dist_info_dict[op_name] = {
                'name': project_name,
                'location': project_location,
            }

        # Provide some log feedback on plugins being used
        for (app_name, info) in iteritems(dist_info_dict):
            LOG.info("Plugins for %-20s: loaded from %-20s - %s." % (app_name,
                info['name'], info['location']))

        return operators_dict

    def get_fluxcd_kustomize_operator(self, app_name):
        """Return a kustomize operator based on app name"""

        plugin_name = utils.find_app_plugin_name(app_name)
        if plugin_name in self.fluxcd_kustomize_operators:
            kustomize_op = self.fluxcd_kustomize_operators[plugin_name]
        else:
            kustomize_op = self.fluxcd_kustomize_operators['generic']
        return kustomize_op

    def _load_helm_applications(self):
        """Build a dictionary of supported helm applications"""

        helm_application_dict = {}
        helm_applications = extension.ExtensionManager(
            namespace=self.STEVEDORE_APPS,
            on_load_failure_callback=suppress_stevedore_errors
        )
        for entry_point in helm_applications.list_entry_points():
            helm_application_dict[entry_point.name] = \
                utils.get_module_name_from_entry_point(entry_point)

        supported_helm_applications = {}
        for name, namespace in helm_application_dict.items():
            supported_helm_applications[name] = []
            helm_plugins = extension.ExtensionManager(
                namespace=namespace, invoke_on_load=True, invoke_args=(self,))
            sorted_helm_plugins = sorted(helm_plugins.extensions, key=lambda x: x.name)
            for plugin in sorted_helm_plugins:
                distribution = utils.get_distribution_from_entry_point(plugin.entry_point)
                (project_name, project_location) = \
                    utils.get_project_name_and_location_from_distribution(distribution)

                LOG.debug("%s: helm plugin %s loaded from %s - %s." % (name,
                    plugin.name,
                    project_name,
                    project_location))

                plugin_name = plugin.name[HELM_PLUGIN_PREFIX_LENGTH:]
                self.chart_operators.update({plugin_name: plugin.obj})
                # Remove duplicates, keeping last occurrence only
                if plugin_name in supported_helm_applications[name]:
                    supported_helm_applications[name].remove(plugin_name)
                supported_helm_applications[name].append(plugin_name)

        return supported_helm_applications

    def get_active_helm_applications(self):
        """ Get the active system applications and charts """
        return self.helm_system_applications

    @property
    def context(self):
        thread_context = eventlet.greenthread.getcurrent()
        return getattr(thread_context, '_helm_context')

    def get_helm_chart_namespaces_by_app(self, chart_name, app_name):
        """Get supported chart namespaces for a given application.

        This method retrieves the namespace supported by a given chart.

        :param chart_name: name of the chart
        :param app_name: name of the application
        :returns: list of supported namespaces that associated overrides may be
                  provided.
        """

        namespaces = []
        if chart_name in self.chart_operators:
            app_plugin_name = utils.find_app_plugin_name(app_name)

            namespaces = self.chart_operators[chart_name].get_namespaces_by_app(
                app_plugin_name)
        return namespaces

    def get_helm_chart_namespaces(self, chart_name):
        """Get supported chart namespaces.

        This method retrieves the namespace supported by a given chart.

        :param chart_name: name of the chart
        :returns: list of supported namespaces that associated overrides may be
                  provided.
        """

        namespaces = []
        if chart_name in self.chart_operators:
            namespaces = self.chart_operators[chart_name].SUPPORTED_NAMESPACES
        return namespaces

    @helm_context
    def get_helm_chart_overrides(self, chart_name, cnamespace=None):
        """ RPCApi: Gets the *chart* overrides for a supported chart. """
        return self._get_helm_chart_overrides(chart_name, cnamespace)

    def _get_helm_chart_overrides(self, chart_name, cnamespace=None):
        """Get the overrides for a supported chart.

        This method retrieves overrides for a supported chart. Overrides for
        all supported namespaces will be returned unless a specific namespace
        is requested.

        :param chart_name: name of a supported chart
        :param cnamespace: (optional) namespace
        :returns: dict of overrides.

        Example Without a cnamespace parameter:
        {
            'kube-system': {
                'deployment': {
                    'mode': 'cluster',
                    'type': 'DaemonSet'
                },
            },
            'openstack': {
                'pod': {
                    'replicas': {
                        'server': 1
                    }
                }
            }
        }

        Example with a cnamespace parameter: cnamespace='kube-system'
        {
            'deployment': {
                'mode': 'cluster',
                'type': 'DaemonSet'
            }
        }
        """
        overrides = {}
        if chart_name in self.chart_operators:
            try:
                overrides.update(
                    self.chart_operators[chart_name].get_overrides(
                        cnamespace))
            except exception.InvalidHelmNamespace:
                raise
        return overrides

    def get_helm_application_namespaces(self, app_name):
        """Get supported application namespaces.

        This method retrieves a dict of charts and their supported namespaces
        for an application.

        :param app_name: name of the bundle of charts required to support an
                         application
        :returns: dict of charts and supported namespaces that associated
                  overrides may be provided.
        """

        app, plugin_name = self._find_kube_app_and_app_plugin_name(app_name)

        app_namespaces = {}
        if plugin_name in self.helm_system_applications:
            for chart_name in self.helm_system_applications[plugin_name]:
                try:
                    app_namespaces.update(
                        {chart_name:
                         self.get_helm_chart_namespaces_by_app(
                             chart_name, app_name)})
                except exception.InvalidHelmNamespace as e:
                    LOG.info(e)
        else:
            # Generic apps
            db_namespaces = self.dbapi.helm_override_get_all(app.id)
            for chart in db_namespaces:
                app_namespaces.setdefault(
                    chart.name, []).append(chart.namespace)

        return app_namespaces

    @helm_context
    def get_helm_application_overrides(self, app_name, cnamespace=None):
        """RPCApi: Gets the application overrides for a supported set of charts."""
        return self._get_helm_application_overrides(app_name, cnamespace)

    def _get_helm_application_overrides(self, app_name, cnamespace=None):
        """Get the overrides for a supported set of charts.

        This method retrieves overrides for a set of supported charts that
        comprise an application. Overrides for all charts and all supported
        namespaces will be returned unless a specific namespace is requested.

        If a specific namespace is requested, then only charts that support
        that specified namespace will be returned.

        :param app_name: name of a supported application (set of charts)
        :param cnamespace: (optional) namespace
        :returns: dict of overrides.

        Example:
        {
            'ingress': {
                'kube-system': {
                    'deployment': {
                        'mode': 'cluster',
                        'type': 'DaemonSet'
                    },
                },
                'openstack': {
                    'pod': {
                        'replicas': {
                            'server': 1
                        }
                    }
                }
            },
            'glance': {
                'openstack': {
                    'pod': {
                        'replicas': {
                            'server': 1
                        }
                    }
                }
             }
        }
        """
        overrides = {}
        plugin_name = utils.find_app_plugin_name(app_name)

        if plugin_name in self.helm_system_applications:
            for chart_name in self.helm_system_applications[plugin_name]:
                try:
                    overrides.update({chart_name:
                                      self._get_helm_chart_overrides(
                                          chart_name,
                                          cnamespace)})
                except exception.InvalidHelmNamespace as e:
                    LOG.info(e)
        return overrides

    def merge_overrides(self, file_overrides=None, set_overrides=None):
        """ Merge helm overrides together.

        :param values: A dict of different types of user override values,
                       'files' (which generally specify many overrides) and
                       'set' (which generally specify one override).
        """

        if file_overrides is None:
            file_overrides = []
        if set_overrides is None:
            set_overrides = []
        # At this point we have potentially two separate types of overrides
        # specified by system or user, values from files and values passed in
        # via --set .  We need to ensure that we call helm using the same
        # mechanisms to ensure the same behaviour.
        args = []

        # Process the newly-passed-in override values
        tmpfiles = []

        for value_file in file_overrides:
            # For values passed in from files, write them back out to
            # temporary files.
            tmpfile = tempfile.NamedTemporaryFile(delete=False)
            tmpfile.write(value_file.encode() if type(value_file) == str else value_file)
            tmpfile.close()
            tmpfiles.append(tmpfile.name)
            args.extend(['--values', tmpfile.name])

        for value_set in set_overrides:
            keypair = list(value_set.split("="))

            # request user to input with "--set key=value" or
            # "--set key=", for the second case, the value is assume ""
            # skip setting like "--set =value", "--set xxxx"
            if len(keypair) == 2 and keypair[0]:
                if keypair[1] and keypair[1].isdigit():
                    args.extend(['--set-string', value_set])
                else:
                    args.extend(['--set', value_set])

        try:
            # Apply changes by calling out to helm to do values merge
            # using a dummy chart.
            output = helm_utils.install_helm_chart_with_dry_run(args)
            # Extract the info we want.
            values = output.split('USER-SUPPLIED VALUES:\n')[1].split(
                '\nCOMPUTED VALUES:')[0]
        except Exception as e:
            LOG.error("Failed to merge overrides %s" % e)
            raise

        for tmpfile in tmpfiles:
            os.remove(tmpfile)

        return values

    @helm_context
    def generate_helm_chart_overrides(self, path, chart_name, cnamespace=None):
        """Generate system helm chart overrides

        This method will generate system helm chart override an write them to a
        yaml file for use with the helm command. If the namespace is provided
        only the overrides file for that specified namespace will be written.

        :param chart_name: name of a supported chart
        :param cnamespace: (optional) namespace
        """

        if chart_name in self.chart_operators:
            namespaces = self.chart_operators[chart_name].SUPPORTED_NAMESPACES
            if cnamespace and cnamespace not in namespaces:
                LOG.exception("The %s chart does not support namespace: %s" %
                              (chart_name, cnamespace))
                return

            try:
                overrides = self._get_helm_chart_overrides(
                    chart_name,
                    cnamespace)
                self._write_chart_overrides(path,
                                            chart_name,
                                            cnamespace,
                                            overrides)
            except Exception as e:
                LOG.exception("failed to create chart overrides for %s: %s" %
                              (chart_name, e))
        elif chart_name:
            LOG.exception("%s chart is not supported" % chart_name)
        else:
            LOG.exception("chart name is required")

    @helm_context
    @utils.synchronized(LOCK_NAME)
    def generate_helm_application_overrides(self, path, app_name,
                                                    mode=None,
                                                    cnamespace=None,
                                                    chart_info=None,
                                                    combined=False):
        """Create the system overrides files for a supported application

        This method will generate system helm chart overrides yaml files for a
        set of supported charts that comprise an application. If the namespace
        is provided only the overrides files for that specified namespace will
        be written.

        :param app_name: name of the bundle of charts required to support an
            application
        :param mode: mode to control how to apply application manifest
        :param cnamespace: (optional) namespace
        :param chart_info: (optional) supporting chart information
            extracted from the fluxcd manifests which is used to influence
            overrides
        :param combined: (optional) whether to apply user overrides on top of
            system overrides
        """

        generated_files = []
        app, plugin_name = self._find_kube_app_and_app_plugin_name(app_name)

        # Get a kustomize operator to provide a single point of
        # manipulation for the chart resources
        kustomize_op = self.get_fluxcd_kustomize_operator(app.name)

        # Load the FluxCD manifests into the operator
        fluxcd_manifests_dir = utils.generate_synced_fluxcd_manifests_fqpn(
            app.name, app.app_version)
        kustomize_op.load(fluxcd_manifests_dir)

        if plugin_name in self.helm_system_applications:
            app_overrides = self._get_helm_application_overrides(plugin_name,
                                                                 cnamespace)
            for (chart_name, overrides) in iteritems(app_overrides):
                if combined:
                    # The overrides at this point are the system overrides. For
                    # charts with multiple namespaces, the overrides would
                    # contain multiple keys, one for each namespace.
                    #
                    # Retrieve the user overrides of each namespace from the
                    # database and merge this list of user overrides, if they
                    # exist, with the system overrides. Both system and user
                    # override contents are then merged based on the namespace,
                    # prepended with required header and written to
                    # corresponding files (<namespace>-<chart>.yaml).
                    file_overrides = []
                    for chart_namespace in overrides.keys():
                        try:
                            db_chart = self.dbapi.helm_override_get(
                                app.id, chart_name, chart_namespace)
                            db_user_overrides = db_chart.user_overrides
                            if db_user_overrides:
                                file_overrides.append(
                                    yaml.dump(
                                        {chart_namespace: yaml.load(
                                            db_user_overrides,
                                            Loader=yaml.FullLoader
                                        )}
                                    )
                                )
                        except exception.HelmOverrideNotFound:
                            pass

                    if file_overrides:
                        # Use dump() instead of safe_dump() as the latter is
                        # not agreeable with password regex in some overrides
                        system_overrides = yaml.dump(overrides)
                        file_overrides.insert(0, system_overrides)
                        combined_overrides = self.merge_overrides(
                            file_overrides=file_overrides)
                        overrides = yaml.load(
                            combined_overrides,
                            Loader=yaml.FullLoader
                        )

                override_file = self._write_chart_overrides(
                    path, chart_name, cnamespace, overrides)
                generated_files.append(override_file)

                # Update manifest docs based on the plugin directives. If the
                # application does not provide a manifest operator, the
                # GenericFluxCDKustomizeOperator is used and chart specific
                # operations can be skipped.
                if kustomize_op.APP:
                    if chart_name in self.chart_operators:
                        self.chart_operators[chart_name].execute_kustomize_updates(
                            kustomize_op)

            # Update the kustomization manifests based on platform conditions
            kustomize_op.platform_mode_kustomize_updates(self.dbapi, mode)

        else:
            # Generic applications
            for chart in chart_info:
                try:
                    db_chart = self.dbapi.helm_override_get(
                        app.id, chart.name, chart.namespace)
                except exception.HelmOverrideNotFound:
                    # This routine is to create helm overrides entries
                    # in database during application-upload so that user
                    # can list the supported helm chart overrides of the
                    # application via helm-override-list
                    try:
                        values = {
                            'name': chart.name,
                            'namespace': chart.namespace,
                            'app_id': app.id,
                        }
                        db_chart = self.dbapi.helm_override_create(values=values)
                    except Exception as e:
                        LOG.exception(e)
                        return

                user_overrides = {chart.namespace: {}}
                db_user_overrides = db_chart.user_overrides
                if db_user_overrides:
                    user_overrides = yaml.load(
                        yaml.dump(
                            {chart.namespace: yaml.load(
                                db_user_overrides,
                                Loader=yaml.FullLoader
                            )}),
                        Loader=yaml.FullLoader
                    )

                override_file = self._write_chart_overrides(
                    path, chart.name, cnamespace, user_overrides)
                generated_files.append(override_file)

        # Write the kustomization doc overrides and a unified manifest for deletion.
        kustomize_op.save_kustomization_updates()
        kustomize_op.save_release_cleanup_data()

        return generated_files

    def _find_kube_app_and_app_plugin_name(self, app_name):
        return utils.find_kube_app(self.dbapi, app_name), \
               utils.find_app_plugin_name(app_name)

    def remove_helm_chart_overrides(self, path, chart_name, cnamespace=None):
        """Remove the overrides files for a chart"""

        if chart_name in self.chart_operators:
            namespaces = self.chart_operators[chart_name].SUPPORTED_NAMESPACES

            filenames = []
            if cnamespace and cnamespace in namespaces:
                filenames.append("%s-%s.yaml" % (cnamespace, chart_name))
            else:
                for n in namespaces:
                    filenames.append("%s-%s.yaml" % (n, chart_name))

            for f in filenames:
                try:
                    self._remove_overrides(path, f)
                except Exception as e:
                    LOG.exception("failed to remove %s overrides: %s: %s" % (
                        chart_name, f, e))
        else:
            LOG.exception("chart %s not supported for system overrides" %
                          chart_name)

    def _write_chart_overrides(self, path, chart_name, cnamespace, overrides):
        """Write a one or more overrides files for a chart. """

        def _write_file(filename, values):
            filepath = ""
            try:
                filepath = self._write_overrides(path, filename, values)
            except Exception as e:
                LOG.exception("failed to write %s overrides: %s: %s" % (
                    chart_name, filename, e))

            return filepath

        filename = helm_utils.build_overrides_filename(chart_name, cnamespace)

        # If the chart has just one namespace there is no need to add
        # the top level reference to it.
        if len(overrides) == 1:
            filepath = _write_file(filename,
                                   overrides[next(iter(overrides))])
        else:
            filepath = _write_file(filename, overrides)

        return filepath

    def _write_overrides(self, path, filename, overrides):
        """Write a single overrides file. """

        if not os.path.isdir(path):
            os.makedirs(path)

        filepath = os.path.join(path, filename)
        try:
            fd, tmppath = tempfile.mkstemp(dir=path, prefix=filename,
                                           text=True)

            with open(tmppath, 'w') as f:
                yaml.dump(overrides, f, default_flow_style=False)
            os.close(fd)
            os.rename(tmppath, filepath)
            # Change the permission to be readable to non-root users
            os.chmod(filepath, 0o644)
        except Exception:
            LOG.exception("failed to write overrides file: %s" % filepath)
            raise

        return filepath

    def _remove_overrides(self, path, filename):
        """Remove a single overrides file. """

        filepath = os.path.join(path, filename)
        try:
            if os.path.exists(filepath):
                os.unlink(filepath)
        except Exception:
            LOG.exception("failed to delete overrides file: %s" % filepath)
            raise

    @helm_context
    def version_check(self, app_name, app_version):
        """Validate application version"""
        if app_name in self.helm_system_applications:
            for chart_name in self.helm_system_applications[app_name]:
                if not self.chart_operators[chart_name].version_check(app_version):
                    LOG.info("Unsupported version reported by %s: %s %s" % (
                             chart_name, app_name, app_version))
                    return False

        # Return True by default
        return True


class HelmOperatorData(HelmOperator):
    """Class to allow retrieval of helm managed data"""

    # TODO (rchurch): decouple. Plugin chart names. This class needs to be
    # delivered as a plugin.
    HELM_CHART_KEYSTONE = 'keystone'
    HELM_CHART_NOVA = 'nova'
    HELM_CHART_CINDER = 'cinder'
    HELM_CHART_GLANCE = 'glance'
    HELM_CHART_NEUTRON = 'neutron'
    HELM_CHART_HEAT = 'heat'
    HELM_CHART_CEILOMETER = 'ceilometer'
    HELM_CHART_DCDBSYNC = 'dcdbsync'

    @helm_context
    def get_keystone_auth_data(self):
        keystone_operator = self.chart_operators[self.HELM_CHART_KEYSTONE]

        # use stx_admin account to communicate with openstack app
        username = common.USER_STX_ADMIN
        try:
            password = keystone_operator.get_stx_admin_password()
        except Exception:
            # old version app doesn't support stx_admin account yet.
            # fallback to admin account
            username = keystone_operator.get_admin_user_name()
            password = keystone_operator.get_admin_password()

        auth_data = {
            'admin_user_name':
                username,
            'admin_project_name':
                keystone_operator.get_admin_project_name(),
            'auth_host':
                'keystone.openstack.svc.cluster.local',
            'auth_port': 80,
            'admin_user_domain':
                keystone_operator.get_admin_user_domain(),
            'admin_project_domain':
                keystone_operator.get_admin_project_domain(),
            'admin_password':
                password,
        }
        return auth_data

    @helm_context
    def get_keystone_endpoint_data(self):
        keystone_operator = self.chart_operators[self.HELM_CHART_KEYSTONE]
        endpoint_data = {
            'endpoint_override':
                'http://keystone.openstack.svc.cluster.local:80',
            'region_name':
                keystone_operator.get_region_name(),
        }
        return endpoint_data

    @helm_context
    def get_keystone_oslo_db_data(self):
        keystone_operator = self.chart_operators[self.HELM_CHART_KEYSTONE]
        endpoints_overrides = keystone_operator.\
            _get_endpoints_oslo_db_overrides(self.HELM_CHART_KEYSTONE,
                                             ['keystone'])

        password = endpoints_overrides['keystone']['password']
        connection = "mysql+pymysql://keystone:%s@" \
                     "mariadb.openstack.svc.cluster.local:3306/keystone"\
                     % (password)

        endpoint_data = {
            'connection':
                connection,
        }
        return endpoint_data

    @helm_context
    def get_nova_endpoint_data(self):
        nova_operator = self.chart_operators[self.HELM_CHART_NOVA]
        endpoint_data = {
            'endpoint_override':
                'http://nova-api-internal.openstack.svc.cluster.local:80',
            'region_name':
                nova_operator.get_region_name(),
        }
        return endpoint_data

    @helm_context
    def get_nova_oslo_messaging_data(self):
        nova_operator = self.chart_operators[self.HELM_CHART_NOVA]
        endpoints_overrides = nova_operator._get_endpoints_overrides()
        auth_data = {
            'host':
                'rabbitmq.openstack.svc.cluster.local',
            'port':
                5672,
            'virt_host':
                'nova',
            'username':
                endpoints_overrides['oslo_messaging']['auth']['nova']
                ['username'],
            'password':
                endpoints_overrides['oslo_messaging']['auth']['nova']
                ['password'],
        }
        return auth_data

    @helm_context
    def get_cinder_endpoint_data(self):
        cinder_operator = self.chart_operators[self.HELM_CHART_CINDER]
        endpoint_data = {
            'region_name':
                cinder_operator.get_region_name(),
            'service_name':
                cinder_operator.get_service_name_v2(),
            'service_type':
                cinder_operator.get_service_type_v2(),
        }
        return endpoint_data

    @helm_context
    def get_glance_endpoint_data(self):
        glance_operator = self.chart_operators[self.HELM_CHART_GLANCE]
        endpoint_data = {
            'region_name':
                glance_operator.get_region_name(),
            'service_name':
                glance_operator.get_service_name(),
            'service_type':
                glance_operator.get_service_type(),
        }
        return endpoint_data

    @helm_context
    def get_neutron_endpoint_data(self):
        neutron_operator = self.chart_operators[self.HELM_CHART_NEUTRON]
        endpoint_data = {
            'region_name':
                neutron_operator.get_region_name(),
        }
        return endpoint_data

    @helm_context
    def get_heat_endpoint_data(self):
        heat_operator = self.chart_operators[self.HELM_CHART_HEAT]
        endpoint_data = {
            'region_name':
                heat_operator.get_region_name(),
        }
        return endpoint_data

    @helm_context
    def get_ceilometer_endpoint_data(self):
        ceilometer_operator = \
            self.chart_operators[self.HELM_CHART_CEILOMETER]
        endpoint_data = {
            'region_name':
                ceilometer_operator.get_region_name(),
        }
        return endpoint_data

    @helm_context
    def get_dcdbsync_endpoint_data(self):
        dcdbsync_operator = self.chart_operators[self.HELM_CHART_DCDBSYNC]
        endpoints_overrides = dcdbsync_operator._get_endpoints_overrides()
        endpoint_data = {
            'keystone_password':
                endpoints_overrides['identity']['auth']['dcdbsync']
                ['password'],
        }
        return endpoint_data
