Merge "Create unit tests for the metadata validation logic"

This commit is contained in:
Zuul 2024-04-18 20:42:33 +00:00 committed by Gerrit Code Review
commit db68afcb23
11 changed files with 613 additions and 137 deletions

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2023 Wind River Systems, Inc.
# Copyright (c) 2023-2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -142,7 +142,7 @@ def validate_metadata_file(path, metadata_file, upgrade_from_release=None):
if not isinstance(value, list):
if not error_message:
error_message = _("Invalid list: {}".format(value))
raise exception.SysinvException(error_message)
raise exception.SysinvException(error_message)
# Field-level validations:
def validate_string_field(parent, key):
@ -433,8 +433,9 @@ def validate_metadata_file(path, metadata_file, upgrade_from_release=None):
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)
validate_list(release_patches,
release_patches_error_message)
for patch in release_patches:
validate_string(patch, patch_error_message)
if release == check_release:

View File

@ -0,0 +1,5 @@
---
app_name: sample-app
app_version: 1.2-3
supported_k8s_version:
minimum: 'v1.2.3'

View File

@ -0,0 +1,20 @@
---
app_name: sample-app
app_version: 1.2-3
supported_k8s_version:
minimum: 'v1.2.3'
behavior:
platform_managed_app: true
desired_state: applied
evaluate_reapply:
after:
- metrics-server.1
- vault.1
- oran.3
triggers:
- type: kube-upgrade-complete
filters:
- availability: services-enabled
- type: host-delete
filters:
- personality: controller

View File

@ -0,0 +1,8 @@
---
app_name: sample-app
app_version: 1.2-3
supported_k8s_version:
minimum: 'v1.2.3'
k8s_upgrades:
auto_update: true
timing: pre

View File

@ -0,0 +1,7 @@
---
app_name: sample-app
app_version: 1.2-3
maintain_user_overrides: true
supported_k8s_version:
minimum: 'v1.2.3'
maximum: 'v1.2.4'

View File

@ -0,0 +1,7 @@
---
app_name: sample-app
app_version: 1.2-3
maintain_user_overrides: true
supported_k8s_version:
minimum: 'v1.2.3'
repo: 'stx-platform'

View File

@ -0,0 +1,9 @@
---
app_name: sample-app
app_version: 1.2-3
supported_k8s_version:
minimum: 'v1.2.3'
supported_releases:
TEST.SW.VERSION:
- "patch_0001"
- "patch_0002"

View File

@ -0,0 +1,11 @@
---
app_name: sample-app
app_version: 1.2-3
supported_k8s_version:
minimum: 'v1.2.3'
upgrades:
auto_update: true
update_failure_no_rollback: true
from_version:
- 1
- 2

View File

@ -0,0 +1,4 @@
---
app_name: sample-app
app_version: 1.2-3
maintain_user_overrides: true

View File

@ -0,0 +1,536 @@
#
# Copyright (c) 2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
"""
Tests for app_metadata.py
"""
import io
import mock
import os.path
import testtools
import yaml
from sysinv.common import app_metadata
from sysinv.common import constants
from sysinv.common import exception
# these unit tests do not need to subclass base.TestCase
class Validate_metadata_file(testtools.TestCase):
def get_metadata_yaml_sample(self, yaml_file_name):
"""Help Funcition to import example yaml
:param yaml_file_name: path to yaml
"""
path = os.path.join(os.path.dirname(__file__),
"data", yaml_file_name)
with open(path, 'r', encoding='utf-8') as f:
yaml_content = f.read()
return yaml_content
def test_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 = \
app_metadata.validate_metadata_file("invalid_path",
"invalid_file",
upgrade_from_release=None)
# if the file is not loaded or has invalid contents
# validate_metadata_file returns two empty strings and
# an empty list ie: "","",[]
self.assertEqual(app_name, "")
self.assertEqual(app_version, "")
self.assertEqual(patches, [])
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
def test_file_validation(self, _mock_isfile, _mock_open):
"""This test mocks file operations
Returns static file contents to allow unit
testing the validation code
"""
_mock_isfile.return_value = "True"
yaml_content = self.get_metadata_yaml_sample('sample_metadata.yaml')
_mock_open.return_value = io.StringIO(yaml_content)
app_name, app_version, patches = \
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_file_validate_with_bad_contents(self,
_mock_isfile,
_mock_open):
"""This test mocks file operations with bad values
This test mocks file operations and verifies
failure handling in how the yaml is validated
"""
_mock_isfile.return_value = "True"
# bad_replacements is a list of atomic changes that
# will trigger a SysinvException in the validator
bad_replacements = [
# app_name cannot be None
{constants.APP_METADATA_NAME: None},
# app_version cannot be None
{constants.APP_METADATA_VERSION: None},
# minimum or maximum cannot be a boolean
{constants.APP_METADATA_SUPPORTED_K8S_VERSION: {
constants.APP_METADATA_MINIMUM: True}},
# minimum or maximum cannot be a number
{constants.APP_METADATA_SUPPORTED_K8S_VERSION: {
constants.APP_METADATA_MINIMUM: 2}},
]
# start each loop with valid contents and replace
# a certain section with bad contents so that the
# validator will raise a SysinvException
for bad_dict in bad_replacements:
contents = self.get_metadata_yaml_sample('sample_metadata.yaml')
contents = yaml.safe_load(contents)
for key, value in bad_dict.items():
contents[key] = value
bad_contents = yaml.dump(contents)
_mock_open.return_value = io.StringIO(bad_contents)
self.assertRaises(exception.SysinvException,
app_metadata.validate_metadata_file,
"valid_path",
"valid_file")
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
def test_file_validation_supported_k8s_version(self,
_mock_isfile,
_mock_open):
"""Validate supported_k8s_version
This test mocks file operations with supported_k8s_version key
and returns static file contents to allow unit
testing the validation code
"""
_mock_isfile.return_value = "True"
yaml_content = self.get_metadata_yaml_sample(
'sample_metadata_k8s_versions.yaml')
_mock_open.return_value = io.StringIO(yaml_content)
app_name, app_version, patches = \
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_file_validation_supported_k8s_version_bad_contents(self,
_mock_isfile,
_mock_open):
"""Validate supported_k8s_version with bad values
This test mocks file operations to supported_k8s_version key
and verifies failure handling in how the yaml is validated
"""
_mock_isfile.return_value = "True"
# bad_replacements is a list of atomic changes that
# will trigger a SysinvException in the validator
bad_replacements = [
# supported_k8s_version must be a dict
{constants.APP_METADATA_SUPPORTED_K8S_VERSION: None},
# minimum must be a string
{constants.APP_METADATA_SUPPORTED_K8S_VERSION: {
constants.APP_METADATA_MINIMUM: True}},
# minimum must be a string
{constants.APP_METADATA_SUPPORTED_K8S_VERSION: {
constants.APP_METADATA_MAXIMUM: 1}}
]
# start each loop with valid contents and replace
# a certain section with bad contents so that the
# validator will raise a SysinvException
for bad_dict in bad_replacements:
contents = self.get_metadata_yaml_sample(
'sample_metadata_k8s_versions.yaml')
contents = yaml.safe_load(contents)
for key, value in bad_dict.items():
contents[key] = value
bad_contents = yaml.dump(contents)
_mock_open.return_value = io.StringIO(bad_contents)
self.assertRaises(exception.SysinvException,
app_metadata.validate_metadata_file,
"valid_path",
"valid_file")
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
def test_file_validation_without_min_k8s_version(self,
_mock_isfile,
_mock_open):
"""Validate supported_k8s_version without supported_k8s_version
This test mocks file operations without supported_k8s_version
key and returns static file contents to allow unit testing the
validation code
"""
_mock_isfile.return_value = "True"
yaml_content = self.get_metadata_yaml_sample(
'sample_metadata_without_k8s_minimum_version.yaml')
_mock_open.return_value = io.StringIO(yaml_content)
self.assertRaises(exception.SysinvException,
app_metadata.validate_metadata_file,
"valid_path",
"valid_file")
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
def test_file_validation_with_upgrade_key(self,
_mock_isfile,
_mock_open):
"""Validate upgrades key
This test mocks file operations with upgrades key
and returns static file contents to allow unit testing
the validation code
"""
_mock_isfile.return_value = "True"
yaml_content = self.get_metadata_yaml_sample('sample_metadata_upgrades.yaml')
_mock_open.return_value = io.StringIO(yaml_content)
app_name, app_version, patches = \
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_file_validation_with_upgrade_key_bad_contents(self,
_mock_isfile,
_mock_open):
"""Validate upgrades key with bad values
This test mocks file operations to upgrades key and
verifies failure handling in how the yaml is validated
"""
_mock_isfile.return_value = "True"
# bad_replacements is a list of atomic changes that
# will trigger a SysinvException in the validator
bad_replacements = [
# upgrades must be a dict
{constants.APP_METADATA_UPGRADES: None},
# update_failure_no_rollback must be a boolean string like: <true/false/yes/no>
{constants.APP_METADATA_UPGRADES: {
constants.APP_METADATA_UPDATE_FAILURE_SKIP_RECOVERY: 123}},
# auto_update must be a boolean string like: <true/false/yes/no>
{constants.APP_METADATA_UPGRADES: {constants.APP_METADATA_AUTO_UPDATE: 123}},
# auto_update must be a list
{constants.APP_METADATA_UPGRADES: {constants.APP_METADATA_FROM_VERSIONS: {}}}
]
# start each loop with valid contents and replace
# a certain section with bad contents so that the
# validator will raise a SysinvException
for bad_dict in bad_replacements:
contents = self.get_metadata_yaml_sample('sample_metadata_upgrades.yaml')
contents = yaml.safe_load(contents)
for key, value in bad_dict.items():
contents[key] = value
bad_contents = yaml.dump(contents)
_mock_open.return_value = io.StringIO(bad_contents)
self.assertRaises(exception.SysinvException,
app_metadata.validate_metadata_file,
"valid_path",
"valid_file")
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
def test_file_validation_repo_key(self,
_mock_isfile,
_mock_open):
"""Validate repo key
This test mocks file operations with repo key and
returns static file contents to allow unit testing
the validation code
"""
_mock_isfile.return_value = "True"
yaml_content = self.get_metadata_yaml_sample('sample_metadata_repo.yaml')
_mock_open.return_value = io.StringIO(yaml_content)
app_name, app_version, patches = \
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_file_validation_behavior_key(self,
_mock_isfile,
_mock_open):
"""Validate behavior key
This test mocks file operations with behavior key and
returns static file contents to allow unit testing
the validation code
"""
_mock_isfile.return_value = "True"
yaml_content = self.get_metadata_yaml_sample('sample_metadata_behavior.yaml')
_mock_open.return_value = io.StringIO(yaml_content)
app_name, app_version, patches = \
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_file_validate_behavior_key_with_bad_contents(self,
_mock_isfile,
_mock_open):
"""Validate behavior key with bad values
This test mocks file operations to behavior key and
verifies failure handling in how the yaml is validated
"""
_mock_isfile.return_value = "True"
# bad_replacements is a list of atomic changes that
# will trigger a SysinvException in the validator
bad_replacements = [
# behavior must be a dict
{constants.APP_METADATA_BEHAVIOR: []},
# platform_managed_app must be a boolean string like: <true/false/yes/no>
{constants.APP_METADATA_BEHAVIOR: {
constants.APP_METADATA_PLATFORM_MANAGED_APP: "something"}},
# desired_state must be a dict
{constants.APP_METADATA_BEHAVIOR: {
constants.APP_METADATA_DESIRED_STATE: 2}},
# evaluate_reapply must be a dict
{constants.APP_METADATA_BEHAVIOR: {constants.APP_METADATA_EVALUATE_REAPPLY: []}},
# after must be a list
{constants.APP_METADATA_BEHAVIOR: {constants.APP_METADATA_EVALUATE_REAPPLY: {
constants.APP_METADATA_AFTER: {}}}},
# triggers must be a list
{constants.APP_METADATA_BEHAVIOR: {constants.APP_METADATA_EVALUATE_REAPPLY: {
constants.APP_METADATA_TRIGGERS: {}}}},
# type must be a string
{constants.APP_METADATA_BEHAVIOR: {
constants.APP_METADATA_EVALUATE_REAPPLY: {
constants.APP_METADATA_TRIGGERS: [{constants.APP_METADATA_TYPE: 1}]
}
}},
# filter_field must be a string
{constants.APP_METADATA_BEHAVIOR: {
constants.APP_METADATA_EVALUATE_REAPPLY: {
constants.APP_METADATA_TRIGGERS: [{constants.APP_METADATA_FILTER_FIELD: 1}]
}
}},
# filters must be a list
{constants.APP_METADATA_BEHAVIOR: {
constants.APP_METADATA_EVALUATE_REAPPLY: {
constants.APP_METADATA_TRIGGERS: [{constants.APP_METADATA_FILTERS: {}}]
}
}}
]
# start each loop with valid contents and replace
# a certain section with bad contents so that the
# validator will raise a SysinvException
for bad_dict in bad_replacements:
contents = self.get_metadata_yaml_sample('sample_metadata_behavior.yaml')
contents = yaml.safe_load(contents)
for key, value in bad_dict.items():
contents[key] = value
bad_contents = yaml.dump(contents)
_mock_open.return_value = io.StringIO(bad_contents)
self.assertRaises(exception.SysinvException,
app_metadata.validate_metadata_file,
"valid_path",
"valid_file")
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
def test_file_validation_supported_releases(self,
_mock_isfile,
_mock_open):
"""Validate supported_releases key
This test mocks file operations with supported_releases
key and returns static file contents to allow unit testing
the validation code
"""
_mock_isfile.return_value = "True"
yaml_content = self.get_metadata_yaml_sample(
'sample_metadata_supported_releases.yaml')
_mock_open.return_value = io.StringIO(yaml_content)
app_name, app_version, patches = \
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")
self.assertEqual(patches, ["patch_0001", "patch_0002"])
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
def test_file_validation_supported_releases_bad_contents(self,
_mock_isfile,
_mock_open):
"""Validate supported_releases key with bad values
This test mocks file operations to supported_releases key
and verifies failure handling in how the yaml is validated
"""
_mock_isfile.return_value = "True"
# bad_replacements is a list of atomic changes that
# will trigger a SysinvException in the validator
bad_replacements = [
# supported_releases must be a dict
{constants.APP_METADATA_SUPPORTED_RELEASES: []},
# releases group must be a list
{constants.APP_METADATA_SUPPORTED_RELEASES: {"TEST.SW.VERSION": {}}},
# releases item must be a string
{constants.APP_METADATA_SUPPORTED_RELEASES: {"TEST.SW.VERSION": [1, True]}},
]
# start each loop with valid contents and replace
# a certain section with bad contents so that the
# validator will raise a SysinvException
for bad_dict in bad_replacements:
contents = self.get_metadata_yaml_sample(
'sample_metadata_supported_releases.yaml')
contents = yaml.safe_load(contents)
for key, value in bad_dict.items():
contents[key] = value
bad_contents = yaml.dump(contents)
_mock_open.return_value = io.StringIO(bad_contents)
self.assertRaises(exception.SysinvException,
app_metadata.validate_metadata_file,
"valid_path",
"valid_file")
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
def test_file_validation_k8s_upgrades(self,
_mock_isfile,
_mock_open):
"""Validate k8s_upgrades key
This test mocks file operations with k8s_upgrades
key and returns static file contents to allow unit testing
the validation code
"""
_mock_isfile.return_value = "True"
yaml_content = self.get_metadata_yaml_sample(
'sample_metadata_k8s_upgrades.yaml')
_mock_open.return_value = io.StringIO(yaml_content)
app_name, app_version, patches = \
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_file_validation_k8s_upgrades_bad_contents(self,
_mock_isfile,
_mock_open):
"""Validate k8s_upgrades key with bad values
This test mocks file operations to k8s_upgrades key
and verifies failure handling in how the yaml is validated
"""
_mock_isfile.return_value = "True"
# bad_replacements is a list of atomic changes that
# will trigger a SysinvException in the validator
bad_replacements = [
# k8s_upgrades must be a dict
{constants.APP_METADATA_K8S_UPGRADES: []},
# auto_update key must be a boolean string
# like: <true/false/yes/no>
{constants.APP_METADATA_K8S_UPGRADES: {
constants.APP_METADATA_AUTO_UPDATE: None,
}},
{constants.APP_METADATA_K8S_UPGRADES: {
constants.APP_METADATA_AUTO_UPDATE: True,
# if auto_update key exist timing key must exist and be a string
constants.APP_METADATA_TIMING: None
}},
]
# start each loop with valid contents and replace
# a certain section with bad contents so that the
# validator will raise a SysinvException
for bad_dict in bad_replacements:
contents = self.get_metadata_yaml_sample(
'sample_metadata_k8s_upgrades.yaml')
contents = yaml.safe_load(contents)
for key, value in bad_dict.items():
contents[key] = value
bad_contents = yaml.dump(contents)
_mock_open.return_value = io.StringIO(bad_contents)
self.assertRaises(exception.SysinvException,
app_metadata.validate_metadata_file,
"valid_path",
"valid_file")

View File

@ -15,26 +15,22 @@
# under the License.
#
#
# Copyright (c) 2022 Wind River Systems, Inc.
# Copyright (c) 2022,2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import errno
import io
import mock
import netaddr
import os
import os.path
import six.moves.builtins as __builtin__
import tempfile
import testtools
import string
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
@ -448,131 +444,3 @@ class IntLikeTestCase(base.TestCase):
self.assertFalse(
utils.is_int_like("0cc3346e-9fef-4445-abe6-5d2b2690ec64"))
self.assertFalse(utils.is_int_like("a1"))
# these unit tests do not need to subclass base.TestCase
class FindMetadataTestCase(testtools.TestCase):
sample_contents = """
app_name: sample-app
app_version: 1.2-3
helm_repo: stx-platform
maintain_user_overrides: true
supported_k8s_version:
minimum: 'v1.2.3'
maximum: 'v2.4.6'
behavior:
platform_managed_app: yes
desired_state: applied
evaluate_reapply:
triggers:
- type: runtime-apply-puppet # TODO(someuser): an inline comment
- type: host-availability-updated
- type: kube-upgrade-complete
filters:
- availability: services-enabled
- type: host-delete
filters:
- personality: controller
"""
bad_contents = """
app_name: sample-app
app_version: 1.2-3
helm_repo: stx-platform
maintain_user_overrides: true
supported_k8s_version:
minimum: 1 # must be a string, not a number
maximum: true # must be a string, not a boolean
behavior:
platform_managed_app: yes
desired_state: applied
evaluate_reapply:
triggers:
- type: runtime-apply-puppet # TODO(someuser): an inline comment
- type: host-availability-updated
- type: kube-upgrade-complete
filters:
- availability: services-enabled
- type: host-delete
filters:
- personality: controller
"""
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 = \
app_metadata.validate_metadata_file("invalid_path",
"invalid_file",
upgrade_from_release=None)
# if the file is not loaded or has invalid contents
# validate_metadata_file returns two empty strings and
# an empty list ie: "","",[]
self.assertEqual(app_name, "")
self.assertEqual(app_version, "")
self.assertEqual(patches, [])
@mock.patch.object(io, 'open')
@mock.patch.object(os.path, 'isfile')
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
"""
_mock_isfile.return_value = "True"
# load fake yaml file contents for: sample-app 1.2-3
_mock_open.return_value = io.StringIO(self.sample_contents)
app_name, app_version, patches = \
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_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
"""
_mock_isfile.return_value = "True"
# bad_replacements is a list of atomic changes that
# will trigger a SysinvException in the validator
bad_replacements = [
# app_name cannot be None
{"app_name": None},
# app_version cannot be None
{"app_version": None},
# behavior must be a dictionary (not a list)
{"behavior": []},
# minimum or maximum cannot be a boolean
{"supported_k8s_version": {"minimum": True, "maximum": "2.4.6"}},
# minimum or maximum cannot be a number
{"supported_k8s_version": {"minimum": "1.2.3", "maximum": 2}},
]
# start each loop with valid contents and replace
# a certain section with bad contents so that the
# validator will raise a SysinvException
for bad_dict in bad_replacements:
contents = yaml.safe_load(self.sample_contents)
for key, value in bad_dict.items():
contents[key] = value
bad_contents = yaml.dump(contents)
_mock_open.return_value = io.StringIO(bad_contents)
self.assertRaises(exception.SysinvException,
app_metadata.validate_metadata_file,
"valid_path",
"valid_file")