354 lines
15 KiB
Python
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
|