config/sysinv/sysinv/sysinv/sysinv/helm/kustomize_base.py

354 lines
15 KiB
Python

#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# All Rights Reserved.
#
""" System inventory FluxCD Kustomize manifest operator."""
import abc
import io
import json
import os
import ruamel.yaml as yaml
import shutil
import six
import tempfile
from copy import deepcopy
from oslo_log import log as logging
from sysinv.common import constants
from sysinv.common import utils as common_utils
from sysinv.db import api as dbapi
LOG = logging.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta)
class FluxCDKustomizeOperator(object):
def __init__(self, manifest_fqpn=None):
self.app_manifest_path = None # Path to the app manifests
self.original_kustomization_fqpn = None # Original kustomization.yaml
self.kustomization_fqpn = None # Updated kustomization.yaml
self.release_cleanup_fqpn = None # Helm release cleanup data
self.helmrepo_path = None # Updated helmrepository.yaml
self.original_helmrepo_fqpn = None # Original helmrepository.yaml
self.kustomization_content = [] # Original app manifest content
self.helmrelease_resource_map = {} # Dict used to disable charts
self.kustomization_resources = [] # Kustomize resource list
self.kustomization_namespace = None # Kustomize global namespace
self.helmrelease_cleanup = [] # List of disabled charts
if manifest_fqpn:
self.load(manifest_fqpn)
def __str__(self):
return json.dumps({
constants.APP_ROOT_KUSTOMIZE_FILE: self.kustomization_content,
'helmrelease_resource_map': self.helmrelease_resource_map,
'helmrelease_cleanup': self.helmrelease_cleanup,
}, indent=2)
def load(self, manifests_dir_fqpn):
""" Load the application kustomization manifests for processing
:param manifest_fqpn: fully qualified path name of the application
manifests directory
"""
# Make sure that the manifests directory exists
if not os.path.exists(manifests_dir_fqpn):
LOG.error("Kustomize manifest directory %s does not exist" %
manifests_dir_fqpn)
return
# Save the location of the application manifests
self.app_manifest_path = manifests_dir_fqpn
self._override_fluxcd_app_repo_url(manifests_dir_fqpn)
# Make sure that the kustomization.yaml file exists
self.kustomization_fqpn = os.path.join(
manifests_dir_fqpn, constants.APP_ROOT_KUSTOMIZE_FILE)
if not os.path.exists(self.kustomization_fqpn):
LOG.error("Kustomize manifest %s does not exist" %
self.kustomization_fqpn)
return
# Save the original kustomization.yaml for
self.original_kustomization_fqpn = "%s-orig%s" % os.path.splitext(
self.kustomization_fqpn)
if not os.path.exists(self.original_kustomization_fqpn):
shutil.copyfile(self.kustomization_fqpn,
self.original_kustomization_fqpn)
# Save the helm release cleanup data file name
self.release_cleanup_fqpn = os.path.join(
manifests_dir_fqpn, constants.APP_RELEASE_CLEANUP_FILE)
# Reset the view of charts to cleanup as platform conditions may have
# changed
self.helmrelease_cleanup = []
# Read the original kustomization.yaml content
with io.open(self.original_kustomization_fqpn, 'r', encoding='utf-8') as f:
# The RoundTripLoader removes the superfluous quotes by default,
# Set preserve_quotes=True to preserve all the quotes.
self.kustomization_content = list(yaml.load_all(
f, Loader=yaml.RoundTripLoader, preserve_quotes=True))
# Expect the top level kustomization.yaml to only have one doc
if len(self.kustomization_content) > 1:
LOG.error("Malformed Kustomize manifest %s contains more than one yaml "
"doc." % self.kustomization_fqpn)
return
# Grab the app resource
self.kustomization_resources = self.kustomization_content[0]['resources']
# Grab the global namespace
self.kustomization_namespace = deepcopy(
self.kustomization_content[0].get("namespace"))
# For these resources, find the HelmRelease info and build a resource
# map
for resource in self.kustomization_resources:
# expect a helmrelease.yaml file to be present in a helm resource
# directory
# is the resource a directory?
resource_fqpn = os.path.join(manifests_dir_fqpn, resource)
if not os.path.isdir(resource_fqpn):
LOG.debug("%s is not a directory and cannot contain HelmRelease "
"info. skipping" % resource_fqpn)
continue
# is a helm release present?
resource_helmrelease_fqpn = os.path.join(resource_fqpn, "helmrelease.yaml")
resource_kustomization_fqpn = os.path.join(resource_fqpn, "kustomization.yaml")
resource_kustomization_namespace = None
if os.path.isfile(resource_helmrelease_fqpn):
with io.open(resource_helmrelease_fqpn, 'r', encoding='utf-8') as f:
resource_helmrelease_contents = list(yaml.load_all(f,
Loader=yaml.RoundTripLoader, preserve_quotes=True))
if len(resource_helmrelease_contents) > 1:
LOG.error("Malformed HelmRelease: %s contains more than one "
"yaml doc." % resource_helmrelease_fqpn)
continue
# get the HelmRelease name
try:
resource_helmrelease_metadata_name = resource_helmrelease_contents[0]['metadata']['name']
resource_helmrelease_namespace = resource_helmrelease_contents[0]['metadata'].get("namespace")
except Exception:
LOG.error("Malformed HelmRelease: Unable to retreive the "
"metadata name from %s" % resource_helmrelease_fqpn)
continue
if os.path.isfile(resource_kustomization_fqpn):
with io.open(resource_kustomization_fqpn, 'r', encoding='utf-8') as fk:
resource_kustomization_contents = list(yaml.load_all(fk,
Loader=yaml.RoundTripLoader, preserve_quotes=True))
if len(resource_kustomization_contents) > 1:
LOG.error("Malformed release Kustomize manifest %s contains more than one yaml "
"doc." % resource_kustomization_fqpn)
continue
resource_kustomization_namespace = resource_kustomization_contents[0].get("namespace")
if resource_kustomization_namespace:
LOG.debug("Using namespace defined on the release's kustomization.yaml")
namespace = resource_kustomization_namespace
elif resource_helmrelease_namespace:
LOG.debug("Using namespace defined on the helmrelease.yaml")
namespace = resource_helmrelease_namespace
elif self.kustomization_namespace:
LOG.debug("Using namespace defined on the top level kustomization.yaml")
namespace = self.kustomization_namespace
else:
LOG.debug("Using fallback namespace")
namespace = constants.FLUXCD_K8S_FALLBACK_NAMESPACE
# Save pertinent data for disabling chart resources and cleaning
# up existing helm releases after being disabled
if resource_helmrelease_metadata_name not in self.helmrelease_resource_map:
self.helmrelease_resource_map[resource_helmrelease_metadata_name] = {
"name": resource_helmrelease_metadata_name,
"namespace": namespace,
"resource": resource,
}
else:
LOG.info("HelmRelease {} on namespace {} already exists. "
"Skipping.".format(resource_helmrelease_metadata_name, namespace))
else:
LOG.debug("Expecting to find a HelmRelease file at {}, skipping "
"resource {}.".format(resource_helmrelease_fqpn,
resource_fqpn))
LOG.info("helmrelease_resource_map: {}".format(self.helmrelease_resource_map))
def _override_fluxcd_app_repo_url(self, manifest):
"""
Replace the host in the default helm repository url
with the network addr floating adress
:param manifest: the manifest dir path
"""
if not os.path.isdir(manifest):
return
self.helmrepo_path = os.path.join(
manifest,
constants.APP_BASE_HELMREPOSITORY_FILE
)
# Save the original kustomization.yaml for
self.original_helmrepo_fqpn = "%s-orig%s" % os.path.splitext(
self.helmrepo_path)
if not os.path.exists(self.original_helmrepo_fqpn):
shutil.copyfile(self.helmrepo_path, self.original_helmrepo_fqpn)
# get the helm repo base url
with io.open(self.original_helmrepo_fqpn, 'r', encoding='utf-8') as f:
helmrepo_yaml = next(yaml.safe_load_all(f))
helmrepo_url = helmrepo_yaml["spec"]["url"]
helmrepo_yaml["spec"]["url"] = \
common_utils.replace_helmrepo_url_with_floating_address(
dbapi.get_instance(), helmrepo_url)
with open(self.helmrepo_path, "w") as f:
yaml.dump(helmrepo_yaml, f, default_flow_style=False)
def _delete_kustomization_file(self):
""" Remove any previously written top level kustomization file
"""
if self.kustomization_fqpn and os.path.exists(self.kustomization_fqpn):
os.remove(self.kustomization_fqpn)
def _delete_release_cleanup_file(self):
""" Remove any previously written helm release cleanup information
"""
if self.release_cleanup_fqpn and os.path.exists(self.release_cleanup_fqpn):
os.remove(self.release_cleanup_fqpn)
def _write_file(self, path, filename, pathfilename, data):
""" Write a yaml file
:param path: path to write the file
:param filename: name of the file
:param pathfilename: FQPN of the file
:param data: file data
"""
try:
fd, tmppath = tempfile.mkstemp(dir=path, prefix=filename,
text=True)
with open(tmppath, 'w') as f:
yaml.dump(data, f, Dumper=yaml.RoundTripDumper,
default_flow_style=False)
os.close(fd)
os.rename(tmppath, pathfilename)
# Change the permission to be readable to non-root
# users
os.chmod(pathfilename, 0o644)
except Exception:
if os.path.exists(tmppath):
os.remove(tmppath)
LOG.exception("Failed to write meta overrides %s" % pathfilename)
raise
def save_kustomization_updates(self):
""" Save an updated top level kustomization.yaml"""
if self.kustomization_fqpn and os.path.exists(self.kustomization_fqpn):
# remove existing kustomization file
self._delete_kustomization_file()
# Save the updated view of the resource to enable
self.kustomization_content[0]['resources'] = self.kustomization_resources
with open(self.kustomization_fqpn, 'w') as f:
try:
yaml.dump_all(self.kustomization_content, f, Dumper=yaml.RoundTripDumper,
explicit_start=True,
default_flow_style=False)
LOG.debug("Updated kustomization file %s generated" %
self.kustomization_fqpn)
except Exception as e:
LOG.error("Failed to generate updated kustomization file %s: "
"%s" % (self.kustomization_fqpn, e))
else:
LOG.error("Kustomization file %s does not exist" % self.kustomization_fqpn)
def save_release_cleanup_data(self):
""" Save yaml to cleanup HelmReleases that are no longer managed."""
# remove existing helm release file
self._delete_release_cleanup_file()
if self.helmrelease_cleanup:
cleanup_dict = {'releases': self.helmrelease_cleanup}
self._write_file(self.app_manifest_path,
constants.APP_RELEASE_CLEANUP_FILE,
self.release_cleanup_fqpn,
cleanup_dict)
else:
LOG.info("%s is not needed. All charts are enabled." % self.release_cleanup_fqpn)
def helm_release_resource_delete(self, helmrelease_name):
""" Delete a helm release resource
This method will remove a chart's resource from the top level
kustomization file which will prevent it from being created during
application applies.
The chart will also be added to a list of charts that will have their
existing helm releases cleaned up
:param helmrelease_name: HelmRelease name to remove from the resource list
"""
removed_resource = self.helmrelease_resource_map.pop(helmrelease_name, None)
if removed_resource:
# Remove the resource from the known resource list
self.kustomization_resources.remove(removed_resource['resource'])
# Save the info needed to clean up any existing chart release
self.helmrelease_cleanup.append({
'name': removed_resource['name'],
'namespace': removed_resource['namespace'],
})
else:
LOG.error("%s is an unknown HelmRelease resource to %s" % (
helmrelease_name, self.original_kustomization_fqpn))
@abc.abstractmethod
def platform_mode_kustomize_updates(self, dbapi, mode):
""" Update the top-level kustomization resource list
Make changes to the top-level kustomization resource list based on the
platform mode
:param dbapi: DB api object
:param mode: mode to control when to update the resource list
"""
pass