Improve application metadata validation code

Improve metadata validation code to:
    1) Provide better reuse of the typing check code within the
       validation method, since there are a number of duplicated type
       checks in place.
    2) Facilitate reusing the validation code in other parts of the
       system. The validation function is lengthy so it was refactored
       and moved to a new file called app_metadata.py.
    3) Add Kubernetes version validation without incurring in huge code
       repetition.
    4) Raise an exception when the k8s_minimum_version section is
       missing from the metadata. This has been introduced and commented
       out for now until at least all default applications have the
       k8s_minimum_version section included on their metadata files.
    5) Raise an exception if the 'auto_update' or 'timing' fields are
       missing from the k8s_upgrades section.

Test Plan:
PASS: build-pkgs -a && build-image
PASS: AIO-SX install
PASS: Upload/apply cert-manager
PASS: Upload/apply platform-integ-apps
PASS: Upload/apply snmp
PASS: Upload/apply a test application that contains the new k8s_upgrade
      metadata section

Story: 2010929
Task: 48948

Change-Id: I6139b8694962855f50c114c4d645b51d7b374f42
Signed-off-by: Igor Soares <Igor.PiresSoares@windriver.com>
This commit is contained in:
Igor Soares 2023-10-12 19:47:35 -03:00 committed by Igor Pires Soares
parent 3511174f95
commit 80f8754d2b
5 changed files with 450 additions and 413 deletions

View File

@ -22,6 +22,7 @@ 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.common import app_metadata
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common import utils as cutils
@ -707,7 +708,7 @@ class KubeAppHelper(object):
def _verify_metadata_file(self, app_path, app_name, app_version,
upgrade_from_release=None):
try:
name, version, patches = cutils.find_metadata_file(
name, version, patches = app_metadata.validate_metadata_file(
app_path, constants.APP_METADATA_FILE,
upgrade_from_release=upgrade_from_release)
except exception.SysinvException as e:

View File

@ -0,0 +1,429 @@
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# All Rights Reserved.
#
import io
import os
import six
import yaml
from oslo_log import log as logging
from sysinv._i18n import _
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common import utils
LOG = logging.getLogger(__name__)
def validate_metadata_file(path, metadata_file, upgrade_from_release=None):
""" 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>
upgrades:
auto_update: <true/false/yes/no>
update_failure_no_rollback: <true/false/yes/no>
from_versions:
- <version.1>
- <version.2>
supported_k8s_version:
minimum: <version>
maximum: <version>
k8s_upgrades:
auto_update: <true/false/yes/no>
timing: <pre/post>
supported_releases:
<release>:
- <patch.1>
- <patch.2>
...
repo: <helm repo> - optional: defaults to HELM_REPO_FOR_APPS
disabled_charts: - optional: charts default to enabled
- <chart name>
- <chart name>
...
maintain_attributes: <true|false>
- optional: defaults to false. Over an app update any system overrides
are preserved for the new version of the application. This can be
renamed to 'maintain_system_overrides', but will require more effort
to keep the naming of 'helm-chart-attribute-modify' command in sync
with this.
maintain_user_overrides: <true|false>
- optional: defaults to false. Over an app update any user overrides are
preserved for the new version of the application
behavior: - optional: describes the app behavior
platform_managed_app: <true/false/yes/no> - optional: when absent
behaves as false
desired_state: <uploaded/applied> - optional: state the app should
reach
evaluate_reapply: - optional: describe the reapply evaluation behaviour
after: - optional: list of apps that should be evaluated before
the current one
- <app_name.1>
- <app_name.2>
triggers: - optional: list of what triggers the reapply evaluation
- type: <key in APP_EVALUATE_REAPPLY_TRIGGER_TO_METADATA_MAP>
filters: - optional: list of field:value, that aid filtering
of the trigger events. All pairs in this list must be
present in trigger dictionary that is passed in
the calls (eg. trigger[field_name1]==value_name1 and
trigger[field_name2]==value_name2).
Function evaluate_apps_reapply takes a dictionary called
'trigger' as parameter. Depending on trigger type this
may contain custom information used by apps, for example
a field 'personality' corresponding to node personality.
It is the duty of the app developer to enhance existing
triggers with the required information.
Hard to obtain information should be passed in the trigger.
To use existing information it is as simple as defining
the metadata.
- <field_name.1>: <value_name.1>
- <field_name.2>: <value_name.2>
filter_field: <field_name> - optional: field name in trigger
dictionary. If specified the filters are applied
to trigger[filter_field] sub-dictionary instead
of the root trigger dictionary.
"""
# Type-level validations:
def validate_string(value, error_message=None):
"""Validate string types"""
if not isinstance(value, six.string_types):
if not error_message:
error_message = _("Invalid string: {}.".format(value))
raise exception.SysinvException(error_message)
def validate_boolstr(value, error_message=None):
"""Validate boolean string types"""
if not utils.is_valid_boolstr(value):
if not error_message:
error_message = _("Invalid boolean value: {}"
.format(value))
raise exception.SysinvException(error_message)
def validate_dict(value, error_message=None):
"""Validate dictionary types"""
if not isinstance(value, dict):
if not error_message:
error_message = _("Invalid dictionary: {}"
.format(value))
raise exception.SysinvException(error_message)
def validate_list(value, error_message=None):
"Validate list types"
if not isinstance(value, list):
if not error_message:
error_message = _("Invalid list: {}".format(value))
raise exception.SysinvException(error_message)
# Field-level validations:
def validate_string_field(parent, key):
""" Validate a metadata string field
:param parent: parent section that contains the string field
to be verified
:param key: field name to be validated
"""
value = None
try:
value = parent[key]
error_message = _("Invalid {}: {} should be {}.".format(
metadata_file,
key,
six.string_types))
validate_string(value, error_message)
except KeyError:
pass
return value
def validate_boolstr_field(parent, key):
""" Validate metadata boolean string fields
:param parent: parent section that contains the boolean string field
to be verified
:param key: field name to be validated
"""
value = None
try:
value = parent[key]
error_message = _("Invalid {}: {} expected values: 'true', 'false', "
"'yes', 'no', 'y', 'n', '1' or '0'"
.format(metadata_file, key))
validate_boolstr(value, error_message)
except KeyError:
pass
return value
def validate_dict_field(parent, key):
""" Validate metadata dictionary fields
:param parent: parent section that contains the dictionary field
to be verified
:param key: field name to be validated
"""
value = None
try:
value = parent[key]
error_message = _("Invalid {}: {} should be a dict."
.format(metadata_file, key))
validate_dict(value, error_message)
except KeyError:
pass
return value
def validate_list_field(parent, key):
""" Validate metadata list fields
:param parent: parent section that contains the list field
to be verified
:param key: field name to be validated
"""
value = None
try:
value = parent[key]
error_message = _("Invalid {}: {} should be a list."
.format(metadata_file,
constants.APP_METADATA_AFTER))
validate_list(value, error_message)
except KeyError:
pass
return value
# Specific validations
def validate_timing(parent):
""" Validate the timing field of a given parent section
:param parent: parent section that contains the timing field
to be verified
"""
value = None
try:
value = \
parent[constants.APP_METADATA_TIMING]
if value != "pre" and value != "post":
raise exception.SysinvException(_(
"Invalid {}: {} expected value is either 'pre' or 'post'."
"".format(metadata_file,
constants.APP_METADATA_TIMING)))
except KeyError:
pass
return value
def validate_k8s_version(parent):
""" Validate the Kubernetes version section of a given
parent section
:param parent: parent section that contains the Kubernetes
version section to be verified
"""
value = \
validate_dict_field(parent,
constants.APP_METADATA_SUPPORTED_K8S_VERSION)
# TODO: uncomment when supported_k8s_version is included on
# the metadata file of at least all default apps
#
# if value is None:
# raise exception.SysinvException(_(
# "Kubernetes supported versions not specified on application "
# "metadata file. Please add a 'supported_k8s_version' section "
# "containing at least a 'minimum' field ('maximum' field is "
# "optional)."))
#
return value
def validate_k8s_minimum_version(parent):
""" Validate the Kubernetes minimum version field of a given
parent section
:param parent: parent section that contains the Kubernetes
minimum version field to be verified
"""
validate_string_field(parent, constants.APP_METADATA_MINIMUM)
# TODO: uncomment when k8s_minimum_version is included on
# the metadata file of at least all default apps
#
# value = validate_string_field(parent, constants.APP_METADATA_MINIMUM)
# if value is None:
# raise exception.SysinvException(_(
# "Minimum supported Kubernetes version not specified "
# "on application metadata file. Please add a 'minimum' "
# "field to the 'supported_k8s_version' section."))
def validate_k8s_upgrades_section(k8s_upgrades_auto_update,
k8s_upgrades_timing):
""" Validate the k8s_upgrade section
:param k8s_app_auto_update: k8s_upgrade:auto_update field value
:param k8s_app_timing: k8s_upgrade:timing field value
"""
if (k8s_upgrades_auto_update and k8s_upgrades_timing is None):
raise exception.SysinvException(_(
"Metadata file has 'k8s_upgrade:auto_update' set but no "
"corresponding k8s_upgrade:timing field was found. Please add "
"a 'timing' field to the 'k8s_upgrade' section."))
if (k8s_upgrades_timing and k8s_upgrades_auto_update is None):
raise exception.SysinvException(_(
"Metadata file has 'k8s_upgrade:timing' set but no "
"corresponding k8s_upgrade:auto_update field was found. Please "
"add an 'auto_update' field to the 'k8s_upgrade' section."))
app_name = ''
app_version = ''
patches = []
metadata_path = os.path.join(path, metadata_file)
if os.path.isfile(metadata_path):
with io.open(metadata_path, 'r', encoding='utf-8') as f:
try:
doc = yaml.safe_load(f)
app_name = doc['app_name']
app_version = doc['app_version']
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))
behavior = validate_dict_field(doc,
constants.APP_METADATA_BEHAVIOR)
if behavior:
validate_boolstr_field(
behavior,
constants.APP_METADATA_PLATFORM_MANAGED_APP)
validate_string_field(
behavior,
constants.APP_METADATA_DESIRED_STATE)
evaluate_reapply = \
validate_dict_field(
behavior,
constants.APP_METADATA_EVALUATE_REAPPLY)
if evaluate_reapply:
validate_list_field(
evaluate_reapply,
constants.APP_METADATA_AFTER)
triggers = validate_list_field(
evaluate_reapply,
constants.APP_METADATA_TRIGGERS)
if triggers:
for trigger in triggers:
validate_dict(trigger)
validate_string_field(
trigger,
constants.APP_METADATA_TYPE)
validate_string_field(
trigger,
constants.APP_METADATA_FILTER_FIELD)
validate_list_field(
trigger,
constants.APP_METADATA_FILTERS)
upgrades = validate_dict_field(doc, constants.APP_METADATA_UPGRADES)
if upgrades:
validate_boolstr_field(
upgrades,
constants.APP_METADATA_UPDATE_FAILURE_SKIP_RECOVERY)
validate_boolstr_field(
upgrades,
constants.APP_METADATA_AUTO_UPDATE)
from_versions = validate_list_field(
upgrades,
constants.APP_METADATA_FROM_VERSIONS)
if from_versions:
for version in from_versions:
validate_string(version)
# Kubernetes version section validation
k8s_version = validate_k8s_version(doc)
if k8s_version:
validate_k8s_minimum_version(k8s_version)
validate_string_field(k8s_version, constants.APP_METADATA_MAXIMUM)
# Kubernetes upgrades section validation
k8s_upgrades = \
validate_dict_field(doc,
constants.APP_METADATA_K8S_UPGRADES)
if k8s_upgrades:
k8s_upgrades_auto_update = \
validate_boolstr_field(k8s_upgrades,
constants.APP_METADATA_AUTO_UPDATE)
k8s_upgrades_timing = validate_timing(k8s_upgrades)
validate_k8s_upgrades_section(k8s_upgrades_auto_update,
k8s_upgrades_timing)
supported_releases = \
validate_dict_field(doc, constants.APP_METADATA_SUPPORTED_RELEASES)
if upgrade_from_release is None:
check_release = utils.get_sw_version()
else:
check_release = upgrade_from_release
if supported_releases:
release_error_message = _(
"Invalid {}: {} release key should be {}."
.format(metadata_file,
constants.APP_METADATA_SUPPORTED_RELEASES,
six.string_types))
release_patches_error_message = _(
"Invalid {}: {} <release>: [<patch>, ...] "
"patches should be a list."
.format(metadata_file,
constants.APP_METADATA_SUPPORTED_RELEASES))
patch_error_message = _(
"Invalid {}: {} <release>: [<patch>, ...] "
"each patch should be {}."
.format(metadata_file,
constants.APP_METADATA_SUPPORTED_RELEASES,
six.string_types))
for release, release_patches in supported_releases.items():
validate_string(release, release_error_message)
validate_list_field(release_patches,
release_patches_error_message)
for patch in release_patches:
validate_string(patch, patch_error_message)
if release == check_release:
patches.extend(release_patches)
LOG.info("{}, application {} ({}), "
"check_release {}, requires patches {}"
.format(metadata_file, app_name, app_version,
check_release, release_patches))
return app_name, app_version, patches

View File

@ -2208,401 +2208,6 @@ def verify_checksum(path):
return rc
def find_metadata_file(path, metadata_file, upgrade_from_release=None):
""" 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>
upgrades:
auto_update: <true/false/yes/no>
update_failure_no_rollback: <true/false/yes/no>
from_versions:
- <version.1>
- <version.2>
supported_k8s_version:
minimum: <version>
maximum: <version>
k8s_upgrades:
auto_update: <true/false/yes/no>
timing: <pre/post>
supported_releases:
<release>:
- <patch.1>
- <patch.2>
...
repo: <helm repo> - optional: defaults to HELM_REPO_FOR_APPS
disabled_charts: - optional: charts default to enabled
- <chart name>
- <chart name>
...
maintain_attributes: <true|false>
- optional: defaults to false. Over an app update any system overrides are
preserved for the new version of the application. This can be renamed to
'maintain_system_overrides', but will require more effort to keep
the naming of 'helm-chart-attribute-modify' command in sync with this.
maintain_user_overrides: <true|false>
- optional: defaults to false. Over an app update any user overrides are
preserved for the new version of the application
behavior: - optional: describes the app behavior
platform_managed_app: <true/false/yes/no> - optional: when absent behaves as false
desired_state: <uploaded/applied> - optional: state the app should reach
evaluate_reapply: - optional: describe the reapply evaluation behaviour
after: - optional: list of apps that should be evaluated before the current one
- <app_name.1>
- <app_name.2>
triggers: - optional: list of what triggers the reapply evaluation
- type: <key in APP_EVALUATE_REAPPLY_TRIGGER_TO_METADATA_MAP>
filters: - optional: list of field:value, that aid filtering
of the trigger events. All pairs in this list must be
present in trigger dictionary that is passed in
the calls (eg. trigger[field_name1]==value_name1 and
trigger[field_name2]==value_name2).
Function evaluate_apps_reapply takes a dictionary called
'trigger' as parameter. Depending on trigger type this
may contain custom information used by apps, for example
a field 'personality' corresponding to node personality.
It is the duty of the app developer to enhance existing
triggers with the required information.
Hard to obtain information should be passed in the trigger.
To use existing information it is as simple as defining
the metadata.
- <field_name.1>: <value_name.1>
- <field_name.2>: <value_name.2>
filter_field: <field_name> - optional: field name in trigger
dictionary. If specified the filters are applied
to trigger[filter_field] sub-dictionary instead
of the root trigger dictionary.
apply_progress_adjust: - optional: Positive integer value by which to adjust the
percentage calculations for the progress of
a monitoring task.
Default value is zero (no adjustment)
"""
def verify_auto_update(parent):
""" Validate the auto_update field of a given parent section
:param parent: parent section that contains the auto_update field
to be verified
"""
try:
auto_update = \
parent[constants.APP_METADATA_AUTO_UPDATE]
if not is_valid_boolstr(auto_update):
raise exception.SysinvException(_(
"Invalid {}: {} expected value is either 'true' "
"or 'false'."
"".format(metadata_file,
constants.APP_METADATA_AUTO_UPDATE)))
except KeyError:
pass
def verify_timing(parent):
""" Validate the timing field of a given parent section
:param parent: parent section that contains the timing field
to be verified
"""
try:
timing = \
parent[constants.APP_METADATA_TIMING]
if timing != "pre" and timing != "post":
raise exception.SysinvException(_(
"Invalid {}: {} expected value is either 'pre' or 'post'."
"".format(metadata_file,
constants.APP_METADATA_TIMING)))
except KeyError:
pass
app_name = ''
app_version = ''
patches = []
metadata_path = os.path.join(path, metadata_file)
if os.path.isfile(metadata_path):
with io.open(metadata_path, 'r', encoding='utf-8') as f:
try:
doc = yaml.safe_load(f)
app_name = doc['app_name']
app_version = doc['app_version']
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))
behavior = None
evaluate_reapply = None
triggers = None
try:
behavior = doc[constants.APP_METADATA_BEHAVIOR]
if not isinstance(behavior, dict):
raise exception.SysinvException(_(
"Invalid {}: {} should be a dict."
"".format(metadata_file,
constants.APP_METADATA_BEHAVIOR)))
except KeyError:
pass
if behavior:
try:
platform_managed_app = behavior[constants.APP_METADATA_PLATFORM_MANAGED_APP]
if not is_valid_boolstr(platform_managed_app):
raise exception.SysinvException(_(
"Invalid {}: {} expected value is a boolean string."
"".format(metadata_file,
constants.APP_METADATA_PLATFORM_MANAGED_APP)))
except KeyError:
pass
try:
desired_state = behavior[constants.APP_METADATA_DESIRED_STATE]
if not isinstance(desired_state, six.string_types):
raise exception.SysinvException(_(
"Invalid {}: {} should be {}."
"".format(metadata_file,
constants.APP_METADATA_DESIRED_STATE,
six.string_types)))
except KeyError:
pass
try:
evaluate_reapply = behavior[constants.APP_METADATA_EVALUATE_REAPPLY]
if not isinstance(evaluate_reapply, dict):
raise exception.SysinvException(_(
"Invalid {}: {} should be a dict."
"".format(metadata_file,
constants.APP_METADATA_EVALUATE_REAPPLY)))
except KeyError:
pass
if evaluate_reapply:
try:
after = evaluate_reapply[constants.APP_METADATA_AFTER]
if not isinstance(after, list):
raise exception.SysinvException(_(
"Invalid {}: {} should be a list."
"".format(metadata_file,
constants.APP_METADATA_AFTER)))
except KeyError:
pass
try:
triggers = evaluate_reapply[constants.APP_METADATA_TRIGGERS]
if not isinstance(triggers, list):
raise exception.SysinvException(_(
"Invalid {}: {} should be a list."
"".format(metadata_file,
constants.APP_METADATA_TRIGGERS)))
except KeyError:
pass
if triggers:
for trigger in triggers:
if not isinstance(trigger, dict):
raise exception.SysinvException(_(
"Invalid {}: element of {} should be a dict."
"".format(metadata_file,
constants.APP_METADATA_TRIGGERS)))
try:
type = trigger[constants.APP_METADATA_TYPE]
if not isinstance(type, six.string_types):
raise exception.SysinvException(_(
"Invalid {}: {} should be {}."
"".format(metadata_file,
constants.APP_METADATA_TYPE,
six.string_types)))
except KeyError:
pass
try:
filter_field = trigger[constants.APP_METADATA_FILTER_FIELD]
if not isinstance(filter_field, six.string_types):
raise exception.SysinvException(_(
"Invalid {}: {} should be {}."
"".format(metadata_file,
constants.APP_METADATA_TYPE,
six.string_types)))
except KeyError:
pass
try:
filters = trigger[constants.APP_METADATA_FILTERS]
if not isinstance(filters, list):
raise exception.SysinvException(_(
"Invalid {}: {} should be a list."
"".format(metadata_file,
constants.APP_METADATA_TYPE)))
except KeyError:
pass
try:
apply_progress_adjust_value = doc[constants.APP_METADATA_APPLY_PROGRESS_ADJUST]
if not isinstance(apply_progress_adjust_value, six.integer_types):
raise exception.SysinvException(_(
"Invalid {}: {} should be {}."
"".format(metadata_file,
constants.APP_METADATA_APPLY_PROGRESS_ADJUST,
six.integer_types)))
if apply_progress_adjust_value < 0:
raise exception.SysinvException(_(
"Invalid {}: {} should be greater or equal to zero."
"".format(metadata_file,
constants.APP_METADATA_APPLY_PROGRESS_ADJUST)))
except KeyError:
pass
upgrades = None
from_versions = []
try:
upgrades = doc[constants.APP_METADATA_UPGRADES]
if not isinstance(upgrades, dict):
raise exception.SysinvException(_(
"Invalid {}: {} should be a dict."
"".format(metadata_file,
constants.APP_METADATA_UPGRADES)))
except KeyError:
pass
if upgrades:
try:
skip_recovery = \
upgrades[constants.APP_METADATA_UPDATE_FAILURE_SKIP_RECOVERY]
if not is_valid_boolstr(skip_recovery):
raise exception.SysinvException(_(
"Invalid {}: {} expected value is a boolean string."
"".format(metadata_file,
constants.APP_METADATA_UPDATE_FAILURE_SKIP_RECOVERY)))
except KeyError:
pass
verify_auto_update(upgrades)
try:
from_versions = upgrades[constants.APP_METADATA_FROM_VERSIONS]
if not isinstance(from_versions, list):
raise exception.SysinvException(_(
"Invalid {}: {} should be a dict."
"".format(metadata_file,
constants.APP_METADATA_FROM_VERSIONS)))
except KeyError:
pass
for version in from_versions:
if not isinstance(version, six.string_types):
raise exception.SysinvException(_(
"Invalid {}: {} each version should be {}."
"".format(metadata_file,
constants.APP_METADATA_FROM_VERSIONS,
six.string_types)))
k8s_version = None
try:
k8s_version = doc[constants.APP_METADATA_SUPPORTED_K8S_VERSION]
if not isinstance(k8s_version, dict):
raise exception.SysinvException(_(
"Invalid {}: {} should be a dict."
"".format(metadata_file,
constants.APP_METADATA_SUPPORTED_K8S_VERSION)))
except KeyError:
pass
if k8s_version:
try:
_minimum = k8s_version[constants.APP_METADATA_MINIMUM]
if not isinstance(_minimum, six.string_types):
raise exception.SysinvException(_(
"Invalid {}: {} should be {}."
"".format(metadata_file,
constants.APP_METADATA_MINIMUM,
six.string_types)))
except KeyError:
pass
try:
_maximum = k8s_version[constants.APP_METADATA_MAXIMUM]
if not isinstance(_maximum, six.string_types):
raise exception.SysinvException(_(
"Invalid {}: {} should be {}."
"".format(metadata_file,
constants.APP_METADATA_MAXIMUM,
six.string_types)))
except KeyError:
pass
k8s_upgrades = None
try:
k8s_upgrades = doc[constants.APP_METADATA_K8S_UPGRADES]
if not isinstance(k8s_upgrades, dict):
raise exception.SysinvException(_(
"Invalid {}: {} should be a dict."
"".format(metadata_file,
constants.APP_METADATA_K8S_UPGRADES)))
except KeyError:
pass
if k8s_upgrades:
verify_auto_update(k8s_upgrades)
verify_timing(k8s_upgrades)
supported_releases = {}
try:
supported_releases = doc[constants.APP_METADATA_SUPPORTED_RELEASES]
if not isinstance(supported_releases, dict):
raise exception.SysinvException(_(
"Invalid {}: {} should be a dict."
"".format(metadata_file,
constants.APP_METADATA_SUPPORTED_RELEASES)))
except KeyError:
pass
if upgrade_from_release is None:
check_release = get_sw_version()
else:
check_release = upgrade_from_release
for release, release_patches in supported_releases.items():
if not isinstance(release, six.string_types):
raise exception.SysinvException(_(
"Invalid {}: {} release key should be {}."
"".format(metadata_file,
constants.APP_METADATA_SUPPORTED_RELEASES,
six.string_types)))
if not isinstance(release_patches, list):
raise exception.SysinvException(_(
"Invalid {}: {} <release>: [<patch>, ...] "
"patches should be a list."
"".format(metadata_file,
constants.APP_METADATA_SUPPORTED_RELEASES)))
for patch in release_patches:
if not isinstance(patch, six.string_types):
raise exception.SysinvException(_(
"Invalid {}: {} <release>: [<patch>, ...] "
"each patch should be {}."
"".format(metadata_file,
constants.APP_METADATA_SUPPORTED_RELEASES,
six.string_types)))
if release == check_release:
patches.extend(release_patches)
LOG.info('{}, application {} ({}), '
'check_release {}, requires patches {}'
''.format(metadata_file, app_name, app_version,
check_release, release_patches))
return app_name, app_version, patches
def find_fluxcd_manifests_directory(path, name):
"""For FluxCD apps we expect to have one top-level manifest directory that
contains the name of constants.APP_FLUXCD_MANIFEST_DIR. Validate that it

View File

@ -43,6 +43,7 @@ from oslo_log import log as logging
from oslo_serialization import base64
from sysinv._i18n import _
from sysinv.api.controllers.v1 import kube_app
from sysinv.common import app_metadata
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common import kubernetes
@ -511,7 +512,7 @@ class AppOperator(object):
# in the next status update
app.regenerate_manifest_filename(mname, os.path.basename(manifest))
else:
name, version, patches = cutils.find_metadata_file(
name, version, patches = app_metadata.validate_metadata_file(
app.inst_path, constants.APP_METADATA_FILE)
app.patch_dependencies = patches

View File

@ -34,6 +34,7 @@ import yaml
from oslo_config import cfg
from sysinv.common import app_metadata
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common import utils
@ -498,18 +499,18 @@ behavior:
- personality: controller
"""
def test_find_metadata_file_nofile(self):
"""Verify results of find_metadata_file
def test_validate_metadata_file_nofile(self):
"""Verify results of validate_metadata_file
when if no file is found, returns:
app_name = "", app_version = "", patches = []
"""
app_name, app_version, patches = \
utils.find_metadata_file("invalid_path",
"invalid_file",
upgrade_from_release=None)
app_metadata.validate_metadata_file("invalid_path",
"invalid_file",
upgrade_from_release=None)
# if the file is not loaded or has invalid contents
# find_metadata_file returns two empty strings and
# validate_metadata_file returns two empty strings and
# an empty list ie: "","",[]
self.assertEqual(app_name, "")
self.assertEqual(app_version, "")
@ -517,9 +518,9 @@ behavior:
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
def test_find_metadata_file(self,
_mock_isfile,
_mock_open):
def test_validate_metadata_file(self,
_mock_isfile,
_mock_open):
"""This test mocks file operations
and returns static file contents to allow unit
testing the validation code
@ -531,17 +532,17 @@ behavior:
_mock_open.return_value = io.StringIO(self.sample_contents)
app_name, app_version, patches = \
utils.find_metadata_file("valid_path",
"valid_file",
upgrade_from_release=None)
app_metadata.validate_metadata_file("valid_path",
"valid_file",
upgrade_from_release=None)
self.assertEqual(app_name, "sample-app")
self.assertEqual(app_version, "1.2-3")
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
def test_find_metadata_file_bad_contents(self,
_mock_isfile,
_mock_open):
def test_validate_metadata_file_bad_contents(self,
_mock_isfile,
_mock_open):
"""This test mocks file operations and verifies
failure handling in how the yaml is validated
"""
@ -572,6 +573,6 @@ behavior:
bad_contents = yaml.dump(contents)
_mock_open.return_value = io.StringIO(bad_contents)
self.assertRaises(exception.SysinvException,
utils.find_metadata_file,
app_metadata.validate_metadata_file,
"valid_path",
"valid_file")