1576 lines
64 KiB
Diff
1576 lines
64 KiB
Diff
From b1539e2dba07f5c0a0d6b76d53bc344890200965 Mon Sep 17 00:00:00 2001
|
|
From: Toshio Kuratomi <tkuratom@redhat.com>
|
|
Date: Thu, 12 Sep 2024 13:25:55 -0700
|
|
Subject: [PATCH 3/6] configs: implement actor configuration support
|
|
|
|
This commit introduces multiple improvements and fixes for actor
|
|
configuration management in LEAPP, including configuration schema
|
|
support, API updates, validation improvements, and compatibility fixes.
|
|
|
|
- Actor Configuration:
|
|
- Add actor configuration support
|
|
- Introduce configuration schema attributes for LEAPP actors.
|
|
- Create an API to load and validate actor configs against the schemas
|
|
stored in actors.
|
|
- Provide a function to retrieve actor-specified configurations.
|
|
- Enable config directories at both repository-wide and actor-wide
|
|
levels.
|
|
- Add configs to `leapp.db`.
|
|
|
|
- Configuration Schema Support:
|
|
- Add `_is_config_sequence()` to validate sequences (lists, tuples) of
|
|
configuration fields.
|
|
- Add support for `StringMap` field types in `config_schemas`.
|
|
|
|
- Testing and Linting:
|
|
- Separate linting and unittests for better CI control.
|
|
|
|
- Dependency Management:
|
|
- Add `pyyaml` to requirements in `requirements.txt`, `setup.py`, and
|
|
spec files.
|
|
|
|
Some unit tests were also updated as they rely on `os.chdir` which
|
|
is problematic as the patch tries to correctly reverse any os.chdir
|
|
that is executed. Therefore, we mock `os.chdir` in the appropriate
|
|
unit tests.
|
|
|
|
JIRA: OAMG-8803
|
|
---
|
|
.github/workflows/unit-tests.yml | 12 +
|
|
etc/leapp/leapp.conf | 2 +
|
|
leapp/actors/__init__.py | 78 +++--
|
|
leapp/actors/config.py | 318 ++++++++++++++++++
|
|
leapp/configs/__init__.py | 0
|
|
leapp/configs/actor/__init__.py | 13 +
|
|
leapp/configs/common/__init__.py | 4 +
|
|
leapp/libraries/stdlib/api.py | 7 +
|
|
leapp/models/fields/__init__.py | 78 ++++-
|
|
leapp/repository/__init__.py | 22 +-
|
|
leapp/repository/actor_definition.py | 41 ++-
|
|
leapp/repository/definition.py | 5 +-
|
|
leapp/repository/scan.py | 16 +
|
|
leapp/utils/audit/__init__.py | 65 +++-
|
|
leapp/workflows/__init__.py | 1 +
|
|
packaging/leapp.spec | 2 +
|
|
requirements.txt | 1 +
|
|
res/container-tests/Containerfile.ubi10 | 13 -
|
|
res/container-tests/Containerfile.ubi10-lint | 35 ++
|
|
res/container-tests/Containerfile.ubi7 | 13 -
|
|
res/container-tests/Containerfile.ubi7-lint | 28 ++
|
|
res/container-tests/Containerfile.ubi8 | 13 -
|
|
res/container-tests/Containerfile.ubi8-lint | 31 ++
|
|
res/container-tests/Containerfile.ubi9 | 13 -
|
|
res/container-tests/Containerfile.ubi9-lint | 30 ++
|
|
res/schema/audit-layout.sql | 13 +-
|
|
.../0003-add-actor-configuration.sql | 16 +
|
|
setup.py | 2 +-
|
|
tests/scripts/test_actor_config_db.py | 90 +++++
|
|
tests/scripts/test_metadata.py | 5 +-
|
|
.../test_repository_actor_definition.py | 6 +-
|
|
31 files changed, 876 insertions(+), 97 deletions(-)
|
|
create mode 100644 leapp/actors/config.py
|
|
create mode 100644 leapp/configs/__init__.py
|
|
create mode 100644 leapp/configs/actor/__init__.py
|
|
create mode 100644 leapp/configs/common/__init__.py
|
|
create mode 100644 res/container-tests/Containerfile.ubi10-lint
|
|
create mode 100644 res/container-tests/Containerfile.ubi7-lint
|
|
create mode 100644 res/container-tests/Containerfile.ubi8-lint
|
|
create mode 100644 res/container-tests/Containerfile.ubi9-lint
|
|
create mode 100644 res/schema/migrations/0003-add-actor-configuration.sql
|
|
create mode 100644 tests/scripts/test_actor_config_db.py
|
|
|
|
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
|
|
index a0a3942..d994b27 100644
|
|
--- a/.github/workflows/unit-tests.yml
|
|
+++ b/.github/workflows/unit-tests.yml
|
|
@@ -18,15 +18,27 @@ jobs:
|
|
- name: Run unit tests with python3.12 on el9
|
|
python: python3.12
|
|
container: ubi10
|
|
+ - name: Run python linters with python3.12 on el9
|
|
+ python: python3.12
|
|
+ container: ubi10-lint
|
|
- name: Run unit tests with python3.9 on el9
|
|
python: python3.9
|
|
container: ubi9
|
|
+ - name: Run python linters with python3.9 on el9
|
|
+ python: python3.9
|
|
+ container: ubi9-lint
|
|
- name: Run unit tests with python 3.6 on el8
|
|
python: python3.6
|
|
container: ubi8
|
|
+ - name: Run python linters with python 3.6 on el8
|
|
+ python: python3.6
|
|
+ container: ubi8-lint
|
|
- name: Run unit tests with python2.7 on el7
|
|
python: python2.7
|
|
container: ubi7
|
|
+ - name: Run python linters with python2.7 on el7
|
|
+ python: python2.7
|
|
+ container: ubi7-lint
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
diff --git a/etc/leapp/leapp.conf b/etc/leapp/leapp.conf
|
|
index d641b30..a96dcce 100644
|
|
--- a/etc/leapp/leapp.conf
|
|
+++ b/etc/leapp/leapp.conf
|
|
@@ -4,3 +4,5 @@ repo_path=/etc/leapp/repos.d/
|
|
[database]
|
|
path=/var/lib/leapp/leapp.db
|
|
|
|
+[actor_config]
|
|
+path=/etc/leapp/actor_conf.d/
|
|
diff --git a/leapp/actors/__init__.py b/leapp/actors/__init__.py
|
|
index 9d83bf1..5970bdb 100644
|
|
--- a/leapp/actors/__init__.py
|
|
+++ b/leapp/actors/__init__.py
|
|
@@ -1,7 +1,16 @@
|
|
+import functools
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
+try:
|
|
+ # Python 3.3+
|
|
+ from collections.abc import Sequence
|
|
+except ImportError:
|
|
+ # Python 2.6 through 3.2
|
|
+ from collections import Sequence
|
|
+
|
|
+from leapp.actors.config import Config, retrieve_config
|
|
from leapp.compat import string_types
|
|
from leapp.dialogs import Dialog
|
|
from leapp.exceptions import (MissingActorAttributeError, RequestStopAfterPhase, StopActorExecution,
|
|
@@ -41,6 +50,11 @@ class Actor(object):
|
|
Write the actor's description as a docstring.
|
|
"""
|
|
|
|
+ config_schemas = ()
|
|
+ """
|
|
+ Defines the structure of the configuration that the actor uses.
|
|
+ """
|
|
+
|
|
consumes = ()
|
|
"""
|
|
Tuple of :py:class:`leapp.models.Model` derived classes defined in the :ref:`repositories <terminology:repository>`
|
|
@@ -86,6 +100,7 @@ class Actor(object):
|
|
'path': os.path.dirname(sys.modules[type(self).__module__].__file__),
|
|
'class_name': type(self).__name__,
|
|
'description': self.description or type(self).__doc__,
|
|
+ 'config_schemas': [c.__name__ for c in self.config_schemas],
|
|
'consumes': [c.__name__ for c in self.consumes],
|
|
'produces': [p.__name__ for p in self.produces],
|
|
'tags': [t.__name__ for t in self.tags],
|
|
@@ -100,6 +115,7 @@ class Actor(object):
|
|
This depends on the definition of such a configuration model being defined by the workflow
|
|
and an actor that provides such a message.
|
|
"""
|
|
+
|
|
Actor.current_instance = self
|
|
install_translation_for_actor(type(self))
|
|
self._messaging = messaging
|
|
@@ -107,8 +123,12 @@ class Actor(object):
|
|
self.skip_dialogs = skip_dialogs
|
|
""" A configured logger instance for the current actor. """
|
|
|
|
+ # self._configuration is the workflow configuration.
|
|
+ # self.config_schemas is the actor defined configuration.
|
|
+ # self.config is the actual actor configuration
|
|
if config_model:
|
|
self._configuration = next(self.consume(config_model), None)
|
|
+ self.config = retrieve_config(self.config_schemas)
|
|
|
|
self._path = path
|
|
|
|
@@ -359,6 +379,15 @@ class Actor(object):
|
|
actor=self,
|
|
details=details)
|
|
|
|
+ def retrieve_config(self):
|
|
+ """
|
|
+ Retrieve the configuration described by self.config_schema.
|
|
+
|
|
+ :return: Dictionary containing requested configuration.
|
|
+ :rtype: dict
|
|
+ """
|
|
+ return retrieve_config(self.config_schema)
|
|
+
|
|
|
|
def _is_type(value_type):
|
|
def validate(actor, name, value):
|
|
@@ -390,17 +419,23 @@ def _lint_warn(actor, name, type_name):
|
|
logging.getLogger("leapp.linter").warning("Actor %s field %s should be a tuple of %s", actor, name, type_name)
|
|
|
|
|
|
-def _is_model_tuple(actor, name, value):
|
|
- if isinstance(value, type) and issubclass(value, Model):
|
|
- _lint_warn(actor, name, "Models")
|
|
+def _is_foo_sequence(cls, cls_name, actor, name, value):
|
|
+ if isinstance(value, type) and issubclass(value, cls):
|
|
+ _lint_warn(actor, name, cls_name)
|
|
value = (value,)
|
|
- _is_type(tuple)(actor, name, value)
|
|
- if not all([True] + [isinstance(item, type) and issubclass(item, Model) for item in value]):
|
|
+ _is_type(Sequence)(actor, name, value)
|
|
+ if not all(isinstance(item, type) and issubclass(item, cls) for item in value):
|
|
raise WrongAttributeTypeError(
|
|
- 'Actor {} attribute {} should contain only Models'.format(actor, name))
|
|
+ 'Actor {} attribute {} should contain only {}'.format(actor, name, cls_name))
|
|
return value
|
|
|
|
|
|
+_is_config_sequence = functools.partial(_is_foo_sequence, Config, "Configs")
|
|
+_is_model_sequence = functools.partial(_is_foo_sequence, Model, "Models")
|
|
+_is_tag_sequence = functools.partial(_is_foo_sequence, Tag, "Tags")
|
|
+_is_api_sequence = functools.partial(_is_foo_sequence, WorkflowAPI, "WorkflowAPIs")
|
|
+
|
|
+
|
|
def _is_dialog_tuple(actor, name, value):
|
|
if isinstance(value, Dialog):
|
|
_lint_warn(actor, name, "Dialogs")
|
|
@@ -412,28 +447,6 @@ def _is_dialog_tuple(actor, name, value):
|
|
return value
|
|
|
|
|
|
-def _is_tag_tuple(actor, name, value):
|
|
- if isinstance(value, type) and issubclass(value, Tag):
|
|
- _lint_warn(actor, name, "Tags")
|
|
- value = (value,)
|
|
- _is_type(tuple)(actor, name, value)
|
|
- if not all([True] + [isinstance(item, type) and issubclass(item, Tag) for item in value]):
|
|
- raise WrongAttributeTypeError(
|
|
- 'Actor {} attribute {} should contain only Tags'.format(actor, name))
|
|
- return value
|
|
-
|
|
-
|
|
-def _is_api_tuple(actor, name, value):
|
|
- if isinstance(value, type) and issubclass(value, WorkflowAPI):
|
|
- _lint_warn(actor, name, "Apis")
|
|
- value = (value,)
|
|
- _is_type(tuple)(actor, name, value)
|
|
- if not all([True] + [isinstance(item, type) and issubclass(item, WorkflowAPI) for item in value]):
|
|
- raise WrongAttributeTypeError(
|
|
- 'Actor {} attribute {} should contain only WorkflowAPIs'.format(actor, name))
|
|
- return value
|
|
-
|
|
-
|
|
def _get_attribute(actor, name, validator, required=False, default_value=None, additional_info='', resolve=None):
|
|
if resolve:
|
|
value = resolve(actor, name)
|
|
@@ -464,13 +477,14 @@ def get_actor_metadata(actor):
|
|
# # if path is not transformed into the realpath.
|
|
('path', os.path.dirname(os.path.realpath(sys.modules[actor.__module__].__file__))),
|
|
_get_attribute(actor, 'name', _is_type(string_types), required=True),
|
|
- _get_attribute(actor, 'tags', _is_tag_tuple, required=True, additional_info=additional_tag_info),
|
|
- _get_attribute(actor, 'consumes', _is_model_tuple, required=False, default_value=(), resolve=get_api_models),
|
|
- _get_attribute(actor, 'produces', _is_model_tuple, required=False, default_value=(), resolve=get_api_models),
|
|
+ _get_attribute(actor, 'tags', _is_tag_sequence, required=True, additional_info=additional_tag_info),
|
|
+ _get_attribute(actor, 'consumes', _is_model_sequence, required=False, default_value=(), resolve=get_api_models),
|
|
+ _get_attribute(actor, 'produces', _is_model_sequence, required=False, default_value=(), resolve=get_api_models),
|
|
_get_attribute(actor, 'dialogs', _is_dialog_tuple, required=False, default_value=()),
|
|
_get_attribute(actor, 'description', _is_type(string_types), required=False,
|
|
default_value=actor.__doc__ or 'There has been no description provided for this actor.'),
|
|
- _get_attribute(actor, 'apis', _is_api_tuple, required=False, default_value=())
|
|
+ _get_attribute(actor, 'config_schemas', _is_config_sequence, required=False, default_value=()),
|
|
+ _get_attribute(actor, 'apis', _is_api_sequence, required=False, default_value=())
|
|
])
|
|
|
|
|
|
diff --git a/leapp/actors/config.py b/leapp/actors/config.py
|
|
new file mode 100644
|
|
index 0000000..160c159
|
|
--- /dev/null
|
|
+++ b/leapp/actors/config.py
|
|
@@ -0,0 +1,318 @@
|
|
+"""
|
|
+Config file format:
|
|
+ yaml file like this:
|
|
+
|
|
+---
|
|
+# Note: have to add a fields.Map type before we can use yaml mappings.
|
|
+section_name:
|
|
+ field1_name: value
|
|
+ field2_name:
|
|
+ - listitem1
|
|
+ - listitem2
|
|
+section2_name:
|
|
+ field3_name: value
|
|
+
|
|
+Config files are any yaml files in /etc/leapp/actor_config.d/
|
|
+(This is settable in /etc/leapp/leapp.conf)
|
|
+
|
|
+"""
|
|
+__metaclass__ = type
|
|
+
|
|
+import abc
|
|
+import glob
|
|
+import logging
|
|
+import os.path
|
|
+from collections import defaultdict
|
|
+
|
|
+import six
|
|
+import yaml
|
|
+
|
|
+from leapp.models.fields import ModelViolationError
|
|
+
|
|
+
|
|
+try:
|
|
+ # Compiled versions if available, for speed
|
|
+ from yaml import CSafeLoader as SafeLoader
|
|
+except ImportError:
|
|
+ from yaml import SafeLoader
|
|
+
|
|
+
|
|
+_ACTOR_CONFIG = None
|
|
+
|
|
+log = logging.getLogger('leapp.actors.config')
|
|
+
|
|
+
|
|
+class SchemaError(Exception):
|
|
+ """
|
|
+ Raised when actors use conflicting schemas.
|
|
+
|
|
+ For example, one schema wants `section.field` to be an integer and other schema wants
|
|
+ `section.field` to be a string.
|
|
+ """
|
|
+
|
|
+
|
|
+class ValidationError(Exception):
|
|
+ """
|
|
+ Raised when a config file fails to validate against any of the available schemas.
|
|
+ """
|
|
+
|
|
+
|
|
+# pylint: disable=deprecated-decorator
|
|
+# @abc.abstractproperty is deprecated in newer Python3 versions but it's
|
|
+# necessary for Python <= 3.3 (including 2.7)
|
|
+@six.add_metaclass(abc.ABCMeta)
|
|
+class Config:
|
|
+ """
|
|
+ An Actor config schema looks like this.
|
|
+
|
|
+ ::
|
|
+ class RHUIConfig(Config):
|
|
+ section = "rhui"
|
|
+ name = "file_map"
|
|
+ type_ = fields.Map(fields.String())
|
|
+ description = 'Description here'
|
|
+ default = {"repo": "url"}
|
|
+ """
|
|
+ @abc.abstractproperty
|
|
+ def section(self):
|
|
+ pass
|
|
+
|
|
+ @abc.abstractproperty
|
|
+ def name(self):
|
|
+ pass
|
|
+
|
|
+ @abc.abstractproperty
|
|
+ def type_(self):
|
|
+ pass
|
|
+
|
|
+ @abc.abstractproperty
|
|
+ def description(self):
|
|
+ pass
|
|
+
|
|
+ @abc.abstractproperty
|
|
+ def default(self):
|
|
+ pass
|
|
+
|
|
+ @classmethod
|
|
+ def to_dict(cls):
|
|
+ """
|
|
+ Return a dictionary representation of the config item that would be suitable for putting
|
|
+ into a config file.
|
|
+ """
|
|
+ representation = {
|
|
+ cls.section: {
|
|
+ '{0}_description__'.format(cls.name): cls.description
|
|
+ }
|
|
+ }
|
|
+ # TODO: Retrieve the default values from the type field.
|
|
+ # Something like this maybe:
|
|
+ # representation[cls.section][cls.name] = cls.type_.get_default()
|
|
+
|
|
+ return representation
|
|
+
|
|
+ @classmethod
|
|
+ def serialize(cls):
|
|
+ """
|
|
+ :return: Serialized information for the config
|
|
+ """
|
|
+ return {
|
|
+ 'class_name': cls.__name__,
|
|
+ 'section': cls.section,
|
|
+ 'name': cls.name,
|
|
+ 'type': cls.type_.serialize(),
|
|
+ 'description': cls.description,
|
|
+ 'default': cls.default,
|
|
+ }
|
|
+# pylint: enable=deprecated-decorator
|
|
+
|
|
+
|
|
+def _merge_config(configuration, new_config):
|
|
+ """
|
|
+ Merge two dictionaries representing configuration. fields in new_config overwrite
|
|
+ any existing fields of the same name in the same section in configuration.
|
|
+ """
|
|
+ for section_name, section in new_config.items():
|
|
+ if section_name not in configuration:
|
|
+ configuration[section_name] = section
|
|
+ else:
|
|
+ for field_name, field in section:
|
|
+ configuration[section_name][field_name] = field
|
|
+
|
|
+
|
|
+def _get_config(config_dir='/etc/leapp/actor_conf.d'):
|
|
+ """
|
|
+ Read all configuration files from the config_dir and return a dict with their values.
|
|
+ """
|
|
+ # We do not do a recursive walk to maintain Python2 compatibility, but we do
|
|
+ # not expect rich config hierarchies, so no harm done.
|
|
+ config_files = glob.glob(os.path.join(config_dir, '*'))
|
|
+ config_files = [f for f in config_files if f.endswith('.yml') or f.endswith('.yaml')]
|
|
+ config_files.sort()
|
|
+
|
|
+ configuration = {}
|
|
+ for config_file in config_files:
|
|
+ with open(config_file) as f:
|
|
+ raw_cfg = f.read()
|
|
+
|
|
+ try:
|
|
+ parsed_config = yaml.load(raw_cfg, SafeLoader)
|
|
+ except Exception as e:
|
|
+ log.warning("Warning: unparsable yaml file %s in the config directory."
|
|
+ " Error: %s", config_file, str(e))
|
|
+ raise
|
|
+
|
|
+ _merge_config(configuration, parsed_config)
|
|
+
|
|
+ return configuration
|
|
+
|
|
+
|
|
+def normalize_schemas(schemas):
|
|
+ """
|
|
+ Merge all schemas into a single dictionary and validate them for errors we can detect.
|
|
+ """
|
|
+ added_fields = set()
|
|
+ normalized_schema = defaultdict(dict)
|
|
+ for schema in schemas:
|
|
+ for field in schema:
|
|
+ unique_name = (field.section, field.name)
|
|
+
|
|
+ # Error if the field has been added by another schema
|
|
+ if unique_name in added_fields and added_fields[unique_name] != field:
|
|
+ # TODO: Also include information on what Actor contains the
|
|
+ # conflicting fields but that information isn't passed into
|
|
+ # this function right now.
|
|
+ message = ('Two actors added incompatible configuration items'
|
|
+ ' with the same name for Section: {section},'
|
|
+ ' Field: {field}'.format(section=field.section,
|
|
+ field=field.name))
|
|
+ log.error(message)
|
|
+ raise SchemaError(message)
|
|
+
|
|
+ # TODO: More validation here.
|
|
+
|
|
+ # Store the fields from the schema in a way that we can easily look
|
|
+ # up while validating
|
|
+ added_fields.add(unique_name)
|
|
+ normalized_schema[field.section][field.name] = field
|
|
+
|
|
+ return normalized_schema
|
|
+
|
|
+
|
|
+def _validate_field_type(field_type, field_value, field_path):
|
|
+ """
|
|
+ Return False if the field is not of the proper type.
|
|
+
|
|
+ :param str field_path: Path in the config where the field is placed.
|
|
+ Example: A field 'target_clients' in a section 'rhui' would have a path 'rhui.target_clients'
|
|
+ """
|
|
+ try:
|
|
+ # the name= parameter is displayed in error messages to let the user know what precisely is wrong
|
|
+ field_type._validate_model_value(field_value, name=field_path)
|
|
+ except ModelViolationError as e:
|
|
+ # Any problems mean that the field did not validate.
|
|
+ log.info("Configuration value failed to validate with: %s", e)
|
|
+ return False
|
|
+ return True
|
|
+
|
|
+
|
|
+def _normalize_config(actor_config, schema):
|
|
+ # Go through the config and log warnings about unexpected sections/fields.
|
|
+ for section_name, section in actor_config.items():
|
|
+ if section_name not in schema:
|
|
+ # TODO: Also have information about which config file contains the unknown field.
|
|
+ message = "A config file contained an unknown section: {section}".format(section=section_name)
|
|
+ log.warning(message)
|
|
+ continue
|
|
+
|
|
+ for field_name in actor_config:
|
|
+ # Any field names which end in "__" are reserved for LEAPP to use
|
|
+ # for its purposes. In particular, it places documentation of
|
|
+ # a field's value into these reserved field names.
|
|
+ if field_name.endswith("__"):
|
|
+ continue
|
|
+
|
|
+ if field_name not in schema[section_name]:
|
|
+ # TODO: Also have information about which config file contains the unknown field.
|
|
+ message = ("A config file contained an unknown field: (Section:"
|
|
+ " {section}, Field: {field})".format(
|
|
+ section=section_name, field=field_name)
|
|
+ )
|
|
+ log.warning(message)
|
|
+
|
|
+ # Do several things:
|
|
+ # * Validate that the config values are of the proper types.
|
|
+ # * Add default values where no config value was provided.
|
|
+ normalized_actor_config = {}
|
|
+ for section_name, section in schema.items():
|
|
+ for field_name, field in section.items():
|
|
+ # TODO: We might be able to do this using the default piece of
|
|
+ # model.fields.Field(). Something using
|
|
+ # schema[section_name, field_name].type_ with the value from
|
|
+ # actor_config[section_name][field_name]. But looking at the Model
|
|
+ # code, I wasn't quite sure how this should be done so I think this
|
|
+ # will work for now.
|
|
+
|
|
+ # For every item in the schema, either retrieve the value from the
|
|
+ # config files or set it to the default.
|
|
+ try:
|
|
+ value = actor_config[section_name][field_name]
|
|
+ except KeyError:
|
|
+ # Either section_name or field_name doesn't exist
|
|
+ section = actor_config[section_name] = actor_config.get(section_name, {})
|
|
+ # May need to deepcopy default if these values are modified.
|
|
+ # However, it's probably an error if they are modified and we
|
|
+ # should possibly look into disallowing that.
|
|
+ value = field.default
|
|
+ section[field_name] = value
|
|
+
|
|
+ field_path = '{0}.{1}'.format(section_name, field_name)
|
|
+ if not _validate_field_type(field.type_, value, field_path):
|
|
+ raise ValidationError("Config value for (Section: {section},"
|
|
+ " Field: {field}) is not of the correct"
|
|
+ " type".format(section=section_name,
|
|
+ field=field_name)
|
|
+ )
|
|
+
|
|
+ normalized_section = normalized_actor_config.get(section_name, {})
|
|
+ normalized_section[field_name] = value
|
|
+ # If the section already exists, this is a no-op. Otherwise, it
|
|
+ # sets it to the newly created dict.
|
|
+ normalized_actor_config[section_name] = normalized_section
|
|
+
|
|
+ return normalized_actor_config
|
|
+
|
|
+
|
|
+def load(config_dir, schemas):
|
|
+ """
|
|
+ Return Actor Configuration.
|
|
+
|
|
+ :returns: a dict representing the configuration.
|
|
+ :raises ValueError: if the actor configuration does not match the schema.
|
|
+
|
|
+ This function reads the config, validates it, and adds any default values.
|
|
+ """
|
|
+ global _ACTOR_CONFIG
|
|
+ if _ACTOR_CONFIG:
|
|
+ return _ACTOR_CONFIG
|
|
+
|
|
+ config = _get_config(config_dir)
|
|
+ config = _normalize_config(config, schemas)
|
|
+
|
|
+ _ACTOR_CONFIG = config
|
|
+ return _ACTOR_CONFIG
|
|
+
|
|
+
|
|
+def retrieve_config(schema):
|
|
+ """Called by the actor to retrieve the actor configuration specific to this actor."""
|
|
+ # TODO: The use of _ACTOR_CONFIG isn't good API. Since this function is
|
|
+ # called by the Actors, we *know* that this is okay to do (as the
|
|
+ # configuration will have already been loaded.) However, there's nothing in
|
|
+ # the API that ensures that this is the case. Need to redesign this.
|
|
+ # Can't think of how it should look right now because loading requires
|
|
+ # information that the Actor doesn't know.
|
|
+
|
|
+ configuration = defaultdict(dict)
|
|
+ for field in schema:
|
|
+ configuration[field.section][field.name] = _ACTOR_CONFIG[field.section][field.name]
|
|
+
|
|
+ return dict(configuration)
|
|
diff --git a/leapp/configs/__init__.py b/leapp/configs/__init__.py
|
|
new file mode 100644
|
|
index 0000000..e69de29
|
|
diff --git a/leapp/configs/actor/__init__.py b/leapp/configs/actor/__init__.py
|
|
new file mode 100644
|
|
index 0000000..37a0c80
|
|
--- /dev/null
|
|
+++ b/leapp/configs/actor/__init__.py
|
|
@@ -0,0 +1,13 @@
|
|
+"""
|
|
+:py:mod:`leapp.configs.actor` represents the import location for private actor config schema that
|
|
+are placed in the actor's configs folder.
|
|
+
|
|
+
|
|
+Example:
|
|
+ If your actor has a configs folder with a schemas.py python module, import it
|
|
+ from the actor like this::
|
|
+
|
|
+ from leapp.configs.actor import schemas
|
|
+
|
|
+This directory is intended for the definitions of actor configuration fields.
|
|
+"""
|
|
diff --git a/leapp/configs/common/__init__.py b/leapp/configs/common/__init__.py
|
|
new file mode 100644
|
|
index 0000000..d41c86c
|
|
--- /dev/null
|
|
+++ b/leapp/configs/common/__init__.py
|
|
@@ -0,0 +1,4 @@
|
|
+"""
|
|
+:py:mod:`leapp.configs.common` represents an import location for shared libraries that
|
|
+are placed in the repository's configs folder.
|
|
+"""
|
|
diff --git a/leapp/libraries/stdlib/api.py b/leapp/libraries/stdlib/api.py
|
|
index c49a03d..267468b 100644
|
|
--- a/leapp/libraries/stdlib/api.py
|
|
+++ b/leapp/libraries/stdlib/api.py
|
|
@@ -228,3 +228,10 @@ def get_actor_tool_path(name):
|
|
:rtype: str or None
|
|
"""
|
|
return current_actor().get_actor_tool_path(name)
|
|
+
|
|
+
|
|
+def retrieve_config():
|
|
+ """
|
|
+ Retrieve the configuration specific to the specified schema.
|
|
+ """
|
|
+ return current_actor().retrieve_config()
|
|
diff --git a/leapp/models/fields/__init__.py b/leapp/models/fields/__init__.py
|
|
index 5e24be4..e5052c6 100644
|
|
--- a/leapp/models/fields/__init__.py
|
|
+++ b/leapp/models/fields/__init__.py
|
|
@@ -2,6 +2,12 @@ import base64
|
|
import copy
|
|
import datetime
|
|
import json
|
|
+try:
|
|
+ # Python 3
|
|
+ from collections.abc import Sequence
|
|
+except ImportError:
|
|
+ # Python 2.7
|
|
+ from collections import Sequence
|
|
|
|
import six
|
|
|
|
@@ -147,7 +153,7 @@ class Field(object):
|
|
|
|
def serialize(self):
|
|
"""
|
|
- :return: Serialized form of the workflow
|
|
+ :return: Serialized form of the field
|
|
"""
|
|
return {
|
|
'nullable': self._nullable,
|
|
@@ -185,11 +191,13 @@ class BuiltinField(Field):
|
|
self._validate(value=value, name=name, expected_type=self._builtin_type)
|
|
|
|
def _validate(self, value, name, expected_type):
|
|
- if not isinstance(expected_type, tuple):
|
|
+ if not isinstance(expected_type, Sequence):
|
|
expected_type = (expected_type,)
|
|
+
|
|
if value is None and self._nullable:
|
|
return
|
|
- if not any(isinstance(value, t) for t in expected_type):
|
|
+
|
|
+ if not isinstance(value, expected_type):
|
|
names = ', '.join(['{}'.format(t.__name__) for t in expected_type])
|
|
raise ModelViolationError("Fields {} is of type: {} expected: {}".format(name, type(value).__name__,
|
|
names))
|
|
@@ -457,6 +465,70 @@ class List(Field):
|
|
return result
|
|
|
|
|
|
+class StringMap(Field):
|
|
+ """
|
|
+ Map from strings to instances of a given value type.
|
|
+ """
|
|
+ def __init__(self, value_type, **kwargs):
|
|
+ super(StringMap, self).__init__(**kwargs)
|
|
+
|
|
+ if self._default is not None:
|
|
+ self._default = copy.copy(self._default)
|
|
+
|
|
+ if not isinstance(value_type, Field):
|
|
+ raise ModelMisuseError("value_type must be an instance of a type derived from Field")
|
|
+
|
|
+ self._value_type = value_type
|
|
+
|
|
+ def _validate_model_value_using_validator(self, new_map, name, validation_method):
|
|
+ list_validator_fn = getattr(super(StringMap, self), validation_method)
|
|
+ list_validator_fn(new_map, name)
|
|
+
|
|
+ if isinstance(new_map, dict):
|
|
+ for key in new_map:
|
|
+ # Check that the key is trully a string
|
|
+ if not isinstance(key, str):
|
|
+ err = 'Expected a key of type `str`, but got a key `{}` of type `{}`'
|
|
+ raise ModelViolationError(err.format(key, type(key).__name__))
|
|
+
|
|
+ value = new_map[key] # avoid using .items(), as it creates a list of all items (slow) in py2
|
|
+
|
|
+ # _value_type's validation will check whether the value has a correct type
|
|
+ value_validator_fn = getattr(self._value_type, validation_method)
|
|
+ value_validator_fn(value, name='{}[{}]'.format(name, key))
|
|
+ elif value is not None:
|
|
+ raise ModelViolationError('Expected a dict but got {} for the {} field'.format(type(value).__name__, name))
|
|
+
|
|
+ def _validate_model_value(self, value, name):
|
|
+ self._validate_model_value_using_validator(value, name, '_validate_model_value')
|
|
+
|
|
+ def _validate_builtin_value(self, value, name):
|
|
+ self._validate_model_value_using_validator(value, name, '_validate_builtin_value')
|
|
+
|
|
+ def _convert_to_model(self, value, name):
|
|
+ self._validate_builtin_value(value=value, name=name)
|
|
+
|
|
+ if value is None:
|
|
+ return value
|
|
+
|
|
+ converter = self._value_type._convert_to_model
|
|
+ return {key: converter(value[key], name='{0}["{1}"]'.format(name, key)) for key in value}
|
|
+
|
|
+ def _convert_from_model(self, value, name):
|
|
+ self._validate_model_value(value=value, name=name)
|
|
+
|
|
+ if value is None:
|
|
+ return value
|
|
+
|
|
+ converter = self._value_type._convert_from_model
|
|
+ return {key: converter(value[key], name='{0}["{1}"]'.format(name, key)) for key in value}
|
|
+
|
|
+ def serialize(self):
|
|
+ result = super(StringMap, self).serialize()
|
|
+ result['value_type'] = self._value_type.serialize()
|
|
+ return result
|
|
+
|
|
+
|
|
class Model(Field):
|
|
"""
|
|
Model is used to use other Models as fields
|
|
diff --git a/leapp/repository/__init__.py b/leapp/repository/__init__.py
|
|
index 58b6d16..8b7aac4 100644
|
|
--- a/leapp/repository/__init__.py
|
|
+++ b/leapp/repository/__init__.py
|
|
@@ -7,6 +7,7 @@ from leapp.compat import load_module
|
|
from leapp.exceptions import RepoItemPathDoesNotExistError, UnsupportedDefinitionKindError
|
|
from leapp.models import get_models, resolve_model_references
|
|
import leapp.libraries.common # noqa # pylint: disable=unused-import
|
|
+import leapp.configs.common # noqa # pylint: disable=unused-import
|
|
from leapp.repository.actor_definition import ActorDefinition
|
|
from leapp.repository.definition import DefinitionKind
|
|
from leapp.tags import get_tags
|
|
@@ -144,8 +145,10 @@ class Repository(object):
|
|
self.log.debug("Extending LEAPP_COMMON_FILES for common file paths")
|
|
self._extend_environ_paths('LEAPP_COMMON_FILES', self.files)
|
|
self.log.debug("Installing repository provided common libraries loader hook")
|
|
+
|
|
sys.meta_path.append(LeappLibrariesFinder(module_prefix='leapp.libraries.common', paths=self.libraries))
|
|
sys.meta_path.append(LeappLibrariesFinder(module_prefix='leapp.workflows.api', paths=self.apis))
|
|
+ sys.meta_path.append(LeappLibrariesFinder(module_prefix='leapp.configs.common', paths=self.configs))
|
|
|
|
if not skip_actors_discovery:
|
|
if not stage or stage is _LoadStage.ACTORS:
|
|
@@ -189,6 +192,15 @@ class Repository(object):
|
|
})
|
|
return data
|
|
|
|
+ # Note: `configs` are not present here because we are not yet making
|
|
+ # them globally accessible. This is to force people to copy the config
|
|
+ # schema to their Actors instead of importing them from other Actors.
|
|
+ # That copy, in turn, is a good idea so the framework can return an
|
|
+ # error if two Actors share the same config but they have different
|
|
+ # schemafor it (for instance, if Actor Foo and Bar were sharing the
|
|
+ # same config entry but Actor Foo updated the entry in a later version.
|
|
+ # We need to error so Actor Bar can either be ported to the new
|
|
+ # definition or use a different config entry for it's needs.)
|
|
return {
|
|
'repo_dir': self._repo_dir,
|
|
'actors': [mapped_actor_data(a.serialize()) for a in self.actors],
|
|
@@ -199,7 +211,8 @@ class Repository(object):
|
|
'workflows': filtered_serialization(get_workflows, self.workflows),
|
|
'tools': [dict([('path', path)]) for path in self.relative_paths(self.tools)],
|
|
'files': [dict([('path', path)]) for path in self.relative_paths(self.files)],
|
|
- 'libraries': [dict([('path', path)]) for path in self.relative_paths(self.libraries)]
|
|
+ 'libraries': [dict([('path', path)]) for path in self.relative_paths(self.libraries)],
|
|
+ 'configs': [dict([('path', path)]) for path in self.relative_paths(self.configs)],
|
|
}
|
|
|
|
def _extend_environ_paths(self, name, paths):
|
|
@@ -268,6 +281,13 @@ class Repository(object):
|
|
"""
|
|
return tuple(self._definitions.get(DefinitionKind.LIBRARIES, ()))
|
|
|
|
+ @property
|
|
+ def configs(self):
|
|
+ """
|
|
+ :return: Tuple of configs in the repository
|
|
+ """
|
|
+ return tuple(self._definitions.get(DefinitionKind.CONFIGS, ()))
|
|
+
|
|
@property
|
|
def files(self):
|
|
"""
|
|
diff --git a/leapp/repository/actor_definition.py b/leapp/repository/actor_definition.py
|
|
index 0e9cc0a..454d7ad 100644
|
|
--- a/leapp/repository/actor_definition.py
|
|
+++ b/leapp/repository/actor_definition.py
|
|
@@ -175,6 +175,7 @@ class ActorDefinition(object):
|
|
'class_name': self.class_name,
|
|
'description': self.description,
|
|
'tags': self.tags,
|
|
+ 'config_schemas': self.config_schemas,
|
|
'consumes': self.consumes,
|
|
'produces': self.produces,
|
|
'apis': self.apis,
|
|
@@ -182,6 +183,7 @@ class ActorDefinition(object):
|
|
'tools': self.tools,
|
|
'files': self.files,
|
|
'libraries': self.libraries,
|
|
+ 'configs': self.configs,
|
|
'tests': self.tests
|
|
}
|
|
|
|
@@ -212,7 +214,21 @@ class ActorDefinition(object):
|
|
if p.exitcode != 0:
|
|
self.log.error("Process inspecting actor in %s failed with %d", self.directory, p.exitcode)
|
|
raise ActorInspectionFailedError('Inspection of actor in {path} failed'.format(path=self.directory))
|
|
- result = q.get()
|
|
+
|
|
+ # Note (dkubek): The use of injected_context() here is necessary
|
|
+ # when retrieving the discovered actor. This is because
|
|
+ # configuration schemas are defined within the actor's scope, and
|
|
+ # how Leapp manages imports. Configuration schemas, being scoped
|
|
+ # within each actor, are accessible internally by the actor itself.
|
|
+ # However, attempting to return a configuration schema outside this
|
|
+ # scope results in an error. This occurs because the schema class
|
|
+ # definition is no longer accessible in the path, making it
|
|
+ # unavailable for importint. The injected_context() here ensures
|
|
+ # the path to the actor level code remains in scope while accessing
|
|
+ # it.
|
|
+ with self.injected_context():
|
|
+ result = q.get()
|
|
+
|
|
if not result:
|
|
self.log.error("Process inspecting actor in %s returned no result", self.directory)
|
|
raise ActorInspectionFailedError(
|
|
@@ -279,6 +295,13 @@ class ActorDefinition(object):
|
|
"""
|
|
return self.discover()['description']
|
|
|
|
+ @property
|
|
+ def config_schemas(self):
|
|
+ """
|
|
+ :return: Actor config_schemas
|
|
+ """
|
|
+ return self.discover()['config_schemas']
|
|
+
|
|
@contextlib.contextmanager
|
|
def injected_context(self):
|
|
"""
|
|
@@ -301,11 +324,18 @@ class ActorDefinition(object):
|
|
if self.tools:
|
|
os.environ['LEAPP_TOOLS'] = os.path.join(self._repo_dir, self._directory, self.tools[0])
|
|
|
|
+ meta_path_backup = sys.meta_path[:]
|
|
+
|
|
sys.meta_path.append(
|
|
LeappLibrariesFinder(
|
|
module_prefix='leapp.libraries.actor',
|
|
paths=[os.path.join(self._repo_dir, self.directory, x) for x in self.libraries]))
|
|
|
|
+ sys.meta_path.append(
|
|
+ LeappLibrariesFinder(
|
|
+ module_prefix='leapp.configs.actor',
|
|
+ paths=[os.path.join(self._repo_dir, self.directory, x) for x in self.configs]))
|
|
+
|
|
previous_path = os.getcwd()
|
|
os.chdir(os.path.join(self._repo_dir, self._directory))
|
|
try:
|
|
@@ -326,6 +356,8 @@ class ActorDefinition(object):
|
|
else:
|
|
os.environ.pop('LEAPP_TOOLS', None)
|
|
|
|
+ sys.meta_path = meta_path_backup
|
|
+
|
|
@property
|
|
def apis(self):
|
|
"""
|
|
@@ -361,6 +393,13 @@ class ActorDefinition(object):
|
|
"""
|
|
return tuple(self._definitions.get(DefinitionKind.FILES, ()))
|
|
|
|
+ @property
|
|
+ def configs(self):
|
|
+ """
|
|
+ :return: Tuple with path to the configs folder of the actor, empty tuple if none
|
|
+ """
|
|
+ return tuple(self._definitions.get(DefinitionKind.CONFIGS, ()))
|
|
+
|
|
@property
|
|
def tests(self):
|
|
"""
|
|
diff --git a/leapp/repository/definition.py b/leapp/repository/definition.py
|
|
index e853a6a..344ec77 100644
|
|
--- a/leapp/repository/definition.py
|
|
+++ b/leapp/repository/definition.py
|
|
@@ -14,8 +14,9 @@ class DefinitionKind(object):
|
|
LIBRARIES = _Kind('libraries')
|
|
TOOLS = _Kind('tools')
|
|
FILES = _Kind('files')
|
|
+ CONFIGS = _Kind('configs')
|
|
TESTS = _Kind('tests')
|
|
API = _Kind('api')
|
|
|
|
- REPO_WHITELIST = (ACTOR, API, MODEL, TOPIC, TAG, WORKFLOW, TOOLS, LIBRARIES, FILES)
|
|
- ACTOR_WHITELIST = (TOOLS, LIBRARIES, FILES, TESTS)
|
|
+ REPO_WHITELIST = (ACTOR, API, MODEL, TOPIC, TAG, WORKFLOW, TOOLS, LIBRARIES, FILES, CONFIGS)
|
|
+ ACTOR_WHITELIST = (TOOLS, LIBRARIES, FILES, CONFIGS, TESTS)
|
|
diff --git a/leapp/repository/scan.py b/leapp/repository/scan.py
|
|
index 378940a..94e0512 100644
|
|
--- a/leapp/repository/scan.py
|
|
+++ b/leapp/repository/scan.py
|
|
@@ -89,6 +89,7 @@ def scan(repository, path):
|
|
('workflows', scan_workflows),
|
|
('files', scan_files),
|
|
('libraries', scan_libraries),
|
|
+ ('configs', scan_configs),
|
|
('tests', scan_tests),
|
|
('tools', scan_tools),
|
|
('apis', scan_apis))
|
|
@@ -224,6 +225,21 @@ def scan_libraries(repo, path, repo_path):
|
|
repo.add(DefinitionKind.LIBRARIES, os.path.relpath(path, repo_path))
|
|
|
|
|
|
+def scan_configs(repo, path, repo_path):
|
|
+ """
|
|
+ Scans configs and adds them to the repository.
|
|
+
|
|
+ :param repo: Instance of the repository
|
|
+ :type repo: :py:class:`leapp.repository.Repository`
|
|
+ :param path: path to the configs
|
|
+ :type path: str
|
|
+ :param repo_path: path to the repository
|
|
+ :type repo_path: str
|
|
+ """
|
|
+ if os.listdir(path):
|
|
+ repo.add(DefinitionKind.CONFIGS, os.path.relpath(path, repo_path))
|
|
+
|
|
+
|
|
def scan_tools(repo, path, repo_path):
|
|
"""
|
|
Scans tools and adds them to the repository.
|
|
diff --git a/leapp/utils/audit/__init__.py b/leapp/utils/audit/__init__.py
|
|
index 16db107..8511179 100644
|
|
--- a/leapp/utils/audit/__init__.py
|
|
+++ b/leapp/utils/audit/__init__.py
|
|
@@ -222,6 +222,67 @@ class DataSource(Host):
|
|
self._data_source_id = cursor.fetchone()[0]
|
|
|
|
|
|
+class ActorConfigData(Storable):
|
|
+ """
|
|
+ Actor configuration data
|
|
+ """
|
|
+
|
|
+ def __init__(self, config=None, hash_id=None):
|
|
+ """
|
|
+ :param config: Actor configuration
|
|
+ :type config: str
|
|
+ :param hash_id: SHA256 hash in hexadecimal representation of config
|
|
+ :type hash_id: str
|
|
+ """
|
|
+ super(ActorConfigData, self).__init__()
|
|
+ self.config = config
|
|
+ self.hash_id = hash_id
|
|
+
|
|
+ def do_store(self, connection):
|
|
+ super(ActorConfigData, self).do_store(connection)
|
|
+ connection.execute(
|
|
+ 'INSERT OR IGNORE INTO actor_config_data (hash, config) VALUES(?, ?)',
|
|
+ (self.hash_id, self.config)
|
|
+ )
|
|
+
|
|
+
|
|
+class ActorConfig(Storable):
|
|
+ """
|
|
+ Actor configuration
|
|
+ """
|
|
+
|
|
+ def __init__(self, context=None, config=None):
|
|
+ """
|
|
+ :param context: The execution context
|
|
+ :type context: str
|
|
+ :param config: Actor configuration
|
|
+ :type config: :py:class:`leapp.utils.audit.ActorConfigData`
|
|
+ """
|
|
+ super(ActorConfig, self).__init__()
|
|
+ self.context = context
|
|
+ self.config = config
|
|
+ self._actor_config_id = None
|
|
+
|
|
+ @property
|
|
+ def actor_config_id(self):
|
|
+ """
|
|
+ Returns the id of the entry, which is only set when already stored.
|
|
+ :return: Integer id or None
|
|
+ """
|
|
+ return self._actor_config_id
|
|
+
|
|
+ def do_store(self, connection):
|
|
+ super(ActorConfig, self).do_store(connection)
|
|
+ self.config.do_store(connection)
|
|
+ connection.execute(
|
|
+ 'INSERT OR IGNORE INTO actor_config (context, actor_config_hash) VALUES(?, ?)',
|
|
+ (self.context, self.config.hash_id))
|
|
+ cursor = connection.execute(
|
|
+ 'SELECT id FROM actor_config WHERE context = ? AND actor_config_hash = ?',
|
|
+ (self.context, self.config.hash_id))
|
|
+ self._actor_config_id = cursor.fetchone()[0]
|
|
+
|
|
+
|
|
class Metadata(Storable):
|
|
"""
|
|
Metadata of an Entity
|
|
@@ -651,10 +712,12 @@ def store_actor_metadata(actor_definition, phase):
|
|
'consumes': sorted(model.__name__ for model in _metadata.get('consumes', ())),
|
|
'produces': sorted(model.__name__ for model in _metadata.get('produces', ())),
|
|
'tags': sorted(tag.__name__ for tag in _metadata.get('tags', ())),
|
|
+ 'config_schemas': [field.serialize() for field in _metadata.get('config_schemas', ())],
|
|
})
|
|
_metadata['phase'] = phase
|
|
|
|
- actor_metadata_fields = ('class_name', 'name', 'description', 'phase', 'tags', 'consumes', 'produces', 'path')
|
|
+ actor_metadata_fields = ('class_name', 'name', 'description', 'phase',
|
|
+ 'tags', 'consumes', 'produces', 'config_schemas', 'path')
|
|
metadata = json.dumps({field: _metadata[field] for field in actor_metadata_fields}, sort_keys=True)
|
|
metadata_hash_id = hashlib.sha256(metadata.encode('utf-8')).hexdigest()
|
|
|
|
diff --git a/leapp/workflows/__init__.py b/leapp/workflows/__init__.py
|
|
index 1b6fc98..b6168ee 100644
|
|
--- a/leapp/workflows/__init__.py
|
|
+++ b/leapp/workflows/__init__.py
|
|
@@ -4,6 +4,7 @@ import socket
|
|
import sys
|
|
import uuid
|
|
|
|
+from leapp.actors.config import retrieve_config
|
|
from leapp.dialogs import RawMessageDialog
|
|
from leapp.exceptions import CommandError, MultipleConfigActorsError, WorkflowConfigNotAvailable
|
|
from leapp.messaging.answerstore import AnswerStore
|
|
diff --git a/packaging/leapp.spec b/packaging/leapp.spec
|
|
index ab38d20..29ece53 100644
|
|
--- a/packaging/leapp.spec
|
|
+++ b/packaging/leapp.spec
|
|
@@ -134,6 +134,7 @@ Provides: leapp-framework-dependencies = %{framework_dependencies}
|
|
Requires: python-six
|
|
Requires: python-setuptools
|
|
Requires: python-requests
|
|
+Requires: PyYAML
|
|
%else # <> rhel 7
|
|
# for Fedora & RHEL 8+ deliver just python3 stuff
|
|
# NOTE: requirement on python3 refers to the general version of Python
|
|
@@ -144,6 +145,7 @@ Requires: python3
|
|
Requires: python3-six
|
|
Requires: python3-setuptools
|
|
Requires: python3-requests
|
|
+Requires: python3-PyYAML
|
|
%endif
|
|
Requires: findutils
|
|
##################################################
|
|
diff --git a/requirements.txt b/requirements.txt
|
|
index ec831ad..2951f23 100644
|
|
--- a/requirements.txt
|
|
+++ b/requirements.txt
|
|
@@ -1,2 +1,3 @@
|
|
six==1.12
|
|
requests
|
|
+PyYAML
|
|
diff --git a/res/container-tests/Containerfile.ubi10 b/res/container-tests/Containerfile.ubi10
|
|
index 6d6507c..1d15d29 100644
|
|
--- a/res/container-tests/Containerfile.ubi10
|
|
+++ b/res/container-tests/Containerfile.ubi10
|
|
@@ -20,19 +20,6 @@ ENTRYPOINT virtualenv testenv -p "/usr/bin/$PYTHON_VENV" && \
|
|
pip install -U pytest && \
|
|
pip install -U six && \
|
|
pip install -U . && \
|
|
- export LINTABLES=$(find . -name '*.py' | grep -E -e '^\./leapp\/' -e '^\./tests/scripts/' | sort -u ) && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo '=============== Running pylint ===============' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo $LINTABLES | xargs pylint && echo '===> pylint PASSED' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo '=============== Running flake8 ===============' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- flake8 $LINTABLES && echo '===> flake8 PASSED' && \
|
|
echo '==================================================' && \
|
|
echo '==================================================' && \
|
|
echo '=============== Running tests ===============' && \
|
|
diff --git a/res/container-tests/Containerfile.ubi10-lint b/res/container-tests/Containerfile.ubi10-lint
|
|
new file mode 100644
|
|
index 0000000..a32e440
|
|
--- /dev/null
|
|
+++ b/res/container-tests/Containerfile.ubi10-lint
|
|
@@ -0,0 +1,35 @@
|
|
+FROM registry.access.redhat.com/ubi9/ubi:latest
|
|
+
|
|
+VOLUME /payload
|
|
+
|
|
+RUN dnf update -y && \
|
|
+ dnf install python3 python312 python3-pip make -y && \
|
|
+ python3 -m pip install --upgrade pip==20.3.4
|
|
+
|
|
+RUN pip install virtualenv
|
|
+
|
|
+WORKDIR /payload
|
|
+ENTRYPOINT virtualenv testenv -p "/usr/bin/$PYTHON_VENV" && \
|
|
+ source testenv/bin/activate && \
|
|
+ pip install -U setuptools && \
|
|
+ pip install -U funcsigs && \
|
|
+ pip install -U -r requirements-tests.txt && \
|
|
+ # NOTE(mmatuska): The pytest ver defined in requirements-tests is too old \
|
|
+ # for Python 3.12 (missing imp module), there do an update here until we \
|
|
+ # bump that. Similarly for six. \
|
|
+ pip install -U pytest && \
|
|
+ pip install -U six && \
|
|
+ pip install -U . && \
|
|
+ export LINTABLES=$(find . -name '*.py' | grep -E -e '^\./leapp\/' -e '^\./tests/scripts/' | sort -u ) && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '=============== Running pylint ===============' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo $LINTABLES | xargs pylint && echo '===> pylint PASSED' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '=============== Running flake8 ===============' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ flake8 $LINTABLES && echo '===> flake8 PASSED'
|
|
diff --git a/res/container-tests/Containerfile.ubi7 b/res/container-tests/Containerfile.ubi7
|
|
index 9e36280..fd5b965 100644
|
|
--- a/res/container-tests/Containerfile.ubi7
|
|
+++ b/res/container-tests/Containerfile.ubi7
|
|
@@ -13,19 +13,6 @@ ENTRYPOINT virtualenv testenv && \
|
|
pip install -U funcsigs && \
|
|
pip install -U -r requirements-tests.txt && \
|
|
pip install -U . && \
|
|
- export LINTABLES=$(find . -name '*.py' | grep -E -e '^\./leapp\/' -e '^\./tests/scripts/' | sort -u ) && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo '=============== Running pylint ===============' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo $LINTABLES | xargs pylint --py3k && echo '===> pylint PASSED' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo '=============== Running flake8 ===============' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- flake8 $LINTABLES && echo '===> flake8 PASSED' && \
|
|
echo '==================================================' && \
|
|
echo '==================================================' && \
|
|
echo '=============== Running tests ===============' && \
|
|
diff --git a/res/container-tests/Containerfile.ubi7-lint b/res/container-tests/Containerfile.ubi7-lint
|
|
new file mode 100644
|
|
index 0000000..2e1de4a
|
|
--- /dev/null
|
|
+++ b/res/container-tests/Containerfile.ubi7-lint
|
|
@@ -0,0 +1,28 @@
|
|
+FROM registry.access.redhat.com/ubi7/ubi:7.9
|
|
+
|
|
+VOLUME /payload
|
|
+
|
|
+RUN yum -y install python27-python-pip && \
|
|
+ scl enable python27 -- pip install -U --target /usr/lib/python2.7/site-packages/ pip==20.3.0 && \
|
|
+ python -m pip install --ignore-installed pip==20.3.4 virtualenv
|
|
+
|
|
+WORKDIR /payload
|
|
+ENTRYPOINT virtualenv testenv && \
|
|
+ source testenv/bin/activate && \
|
|
+ pip install -U setuptools && \
|
|
+ pip install -U funcsigs && \
|
|
+ pip install -U -r requirements-tests.txt && \
|
|
+ pip install -U . && \
|
|
+ export LINTABLES=$(find . -name '*.py' | grep -E -e '^\./leapp\/' -e '^\./tests/scripts/' | sort -u ) && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '=============== Running pylint ===============' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo $LINTABLES | xargs pylint --py3k && echo '===> pylint PASSED' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '=============== Running flake8 ===============' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ flake8 $LINTABLES && echo '===> flake8 PASSED'
|
|
diff --git a/res/container-tests/Containerfile.ubi8 b/res/container-tests/Containerfile.ubi8
|
|
index 03d499d..960f4b0 100644
|
|
--- a/res/container-tests/Containerfile.ubi8
|
|
+++ b/res/container-tests/Containerfile.ubi8
|
|
@@ -16,19 +16,6 @@ ENTRYPOINT virtualenv testenv -p "/usr/bin/$PYTHON_VENV" && \
|
|
pip install -U funcsigs && \
|
|
pip install -U -r requirements-tests.txt && \
|
|
pip install -U . && \
|
|
- export LINTABLES=$(find . -name '*.py' | grep -E -e '^\./leapp\/' -e '^\./tests/scripts/' | sort -u ) && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo '=============== Running pylint ===============' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo $LINTABLES | xargs pylint && echo '===> pylint PASSED' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo '=============== Running flake8 ===============' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- flake8 $LINTABLES && echo '===> flake8 PASSED' && \
|
|
echo '==================================================' && \
|
|
echo '==================================================' && \
|
|
echo '=============== Running tests ===============' && \
|
|
diff --git a/res/container-tests/Containerfile.ubi8-lint b/res/container-tests/Containerfile.ubi8-lint
|
|
new file mode 100644
|
|
index 0000000..75143d9
|
|
--- /dev/null
|
|
+++ b/res/container-tests/Containerfile.ubi8-lint
|
|
@@ -0,0 +1,31 @@
|
|
+FROM registry.access.redhat.com/ubi8/ubi:latest
|
|
+
|
|
+VOLUME /payload
|
|
+
|
|
+ENV PYTHON_VENV "python3.6"
|
|
+
|
|
+RUN yum update -y && \
|
|
+ yum install python3 python39 python3-virtualenv make -y && \
|
|
+ yum -y install python3-pip && \
|
|
+ python3 -m pip install --upgrade pip==20.3.4
|
|
+
|
|
+WORKDIR /payload
|
|
+ENTRYPOINT virtualenv testenv -p "/usr/bin/$PYTHON_VENV" && \
|
|
+ source testenv/bin/activate && \
|
|
+ pip install -U setuptools && \
|
|
+ pip install -U funcsigs && \
|
|
+ pip install -U -r requirements-tests.txt && \
|
|
+ pip install -U . && \
|
|
+ export LINTABLES=$(find . -name '*.py' | grep -E -e '^\./leapp\/' -e '^\./tests/scripts/' | sort -u ) && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '=============== Running pylint ===============' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo $LINTABLES | xargs pylint && echo '===> pylint PASSED' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '=============== Running flake8 ===============' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ flake8 $LINTABLES && echo '===> flake8 PASSED'
|
|
diff --git a/res/container-tests/Containerfile.ubi9 b/res/container-tests/Containerfile.ubi9
|
|
index b1b5b71..755e874 100644
|
|
--- a/res/container-tests/Containerfile.ubi9
|
|
+++ b/res/container-tests/Containerfile.ubi9
|
|
@@ -15,19 +15,6 @@ ENTRYPOINT virtualenv testenv -p "/usr/bin/$PYTHON_VENV" && \
|
|
pip install -U funcsigs && \
|
|
pip install -U -r requirements-tests.txt && \
|
|
pip install -U . && \
|
|
- export LINTABLES=$(find . -name '*.py' | grep -E -e '^\./leapp\/' -e '^\./tests/scripts/' | sort -u ) && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo '=============== Running pylint ===============' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo $LINTABLES | xargs pylint && echo '===> pylint PASSED' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- echo '=============== Running flake8 ===============' && \
|
|
- echo '==================================================' && \
|
|
- echo '==================================================' && \
|
|
- flake8 $LINTABLES && echo '===> flake8 PASSED' && \
|
|
echo '==================================================' && \
|
|
echo '==================================================' && \
|
|
echo '=============== Running tests ===============' && \
|
|
diff --git a/res/container-tests/Containerfile.ubi9-lint b/res/container-tests/Containerfile.ubi9-lint
|
|
new file mode 100644
|
|
index 0000000..9a1d186
|
|
--- /dev/null
|
|
+++ b/res/container-tests/Containerfile.ubi9-lint
|
|
@@ -0,0 +1,30 @@
|
|
+FROM registry.access.redhat.com/ubi9/ubi:latest
|
|
+
|
|
+VOLUME /payload
|
|
+
|
|
+RUN dnf update -y && \
|
|
+ dnf install python3 python39 make python3-pip -y && \
|
|
+ python3 -m pip install --upgrade pip==20.3.4
|
|
+
|
|
+RUN pip install virtualenv
|
|
+
|
|
+WORKDIR /payload
|
|
+ENTRYPOINT virtualenv testenv -p "/usr/bin/$PYTHON_VENV" && \
|
|
+ source testenv/bin/activate && \
|
|
+ pip install -U setuptools && \
|
|
+ pip install -U funcsigs && \
|
|
+ pip install -U -r requirements-tests.txt && \
|
|
+ pip install -U . && \
|
|
+ export LINTABLES=$(find . -name '*.py' | grep -E -e '^\./leapp\/' -e '^\./tests/scripts/' | sort -u ) && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '=============== Running pylint ===============' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo $LINTABLES | xargs pylint && echo '===> pylint PASSED' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '=============== Running flake8 ===============' && \
|
|
+ echo '==================================================' && \
|
|
+ echo '==================================================' && \
|
|
+ flake8 $LINTABLES && echo '===> flake8 PASSED'
|
|
diff --git a/res/schema/audit-layout.sql b/res/schema/audit-layout.sql
|
|
index d567ce4..93a92f1 100644
|
|
--- a/res/schema/audit-layout.sql
|
|
+++ b/res/schema/audit-layout.sql
|
|
@@ -1,6 +1,6 @@
|
|
BEGIN;
|
|
|
|
-PRAGMA user_version = 3;
|
|
+PRAGMA user_version = 4;
|
|
|
|
CREATE TABLE IF NOT EXISTS execution (
|
|
id INTEGER PRIMARY KEY NOT NULL,
|
|
@@ -47,6 +47,17 @@ CREATE TABLE IF NOT EXISTS metadata (
|
|
metadata TEXT
|
|
);
|
|
|
|
+CREATE TABLE IF NOT EXISTS actor_config (
|
|
+ id INTEGER PRIMARY KEY NOT NULL,
|
|
+ context VARCHAR(36) NOT NULL REFERENCES execution (context),
|
|
+ actor_config_hash VARCHAR(64) NOT NULL REFERENCES actor_config_data (hash)
|
|
+);
|
|
+
|
|
+CREATE TABLE IF NOT EXISTS actor_config_data (
|
|
+ hash VARCHAR(64) PRIMARY KEY NOT NULL,
|
|
+ config TEXT
|
|
+);
|
|
+
|
|
CREATE TABLE IF NOT EXISTS entity (
|
|
id INTEGER PRIMARY KEY NOT NULL,
|
|
context VARCHAR(36) NOT NULL REFERENCES execution (context),
|
|
diff --git a/res/schema/migrations/0003-add-actor-configuration.sql b/res/schema/migrations/0003-add-actor-configuration.sql
|
|
new file mode 100644
|
|
index 0000000..11131c8
|
|
--- /dev/null
|
|
+++ b/res/schema/migrations/0003-add-actor-configuration.sql
|
|
@@ -0,0 +1,16 @@
|
|
+BEGIN;
|
|
+
|
|
+CREATE TABLE IF NOT EXISTS actor_config (
|
|
+ id INTEGER PRIMARY KEY NOT NULL,
|
|
+ context VARCHAR(36) NOT NULL REFERENCES execution (context),
|
|
+ actor_config_hash VARCHAR(64) NOT NULL REFERENCES actor_config_data (hash)
|
|
+);
|
|
+
|
|
+CREATE TABLE IF NOT EXISTS actor_config_data (
|
|
+ hash VARCHAR(64) PRIMARY KEY NOT NULL,
|
|
+ config TEXT
|
|
+);
|
|
+
|
|
+PRAGMA user_version = 4;
|
|
+
|
|
+COMMIT;
|
|
diff --git a/setup.py b/setup.py
|
|
index 40e7656..3ebe8b2 100644
|
|
--- a/setup.py
|
|
+++ b/setup.py
|
|
@@ -23,7 +23,7 @@ setup(
|
|
name='leapp',
|
|
version=main_ns['VERSION'],
|
|
packages=find_packages(exclude=EXCLUSION),
|
|
- install_requires=['six', 'requests'],
|
|
+ install_requires=['six', 'requests', 'PyYAML'],
|
|
entry_points='''
|
|
[console_scripts]
|
|
snactor=leapp.snactor:main
|
|
diff --git a/tests/scripts/test_actor_config_db.py b/tests/scripts/test_actor_config_db.py
|
|
new file mode 100644
|
|
index 0000000..75d6676
|
|
--- /dev/null
|
|
+++ b/tests/scripts/test_actor_config_db.py
|
|
@@ -0,0 +1,90 @@
|
|
+import os
|
|
+import hashlib
|
|
+import json
|
|
+
|
|
+from leapp.utils.audit import (get_connection, dict_factory, ActorConfigData, ActorConfig)
|
|
+from leapp.config import get_config
|
|
+
|
|
+_CONTEXT_NAME = 'test-context-name-actor-config-db'
|
|
+_TEST_ACTOR_CONFIG = {}
|
|
+
|
|
+
|
|
+def setup_module():
|
|
+ get_config().set('database', 'path', '/tmp/leapp-test.db')
|
|
+
|
|
+
|
|
+def setup():
|
|
+ path = get_config().get('database', 'path')
|
|
+ if os.path.isfile(path):
|
|
+ os.unlink(path)
|
|
+
|
|
+
|
|
+def test_save_empty_actor_config_data():
|
|
+ hash_id = hashlib.sha256('test-empty-actor-config'.encode('utf-8')).hexdigest()
|
|
+ cfg = ActorConfigData(hash_id=hash_id, config='')
|
|
+ cfg.store()
|
|
+
|
|
+ entry = None
|
|
+ with get_connection(None) as conn:
|
|
+ cursor = conn.execute('SELECT * FROM actor_config_data WHERE hash = ?;', (hash_id,))
|
|
+ cursor.row_factory = dict_factory
|
|
+ entry = cursor.fetchone()
|
|
+
|
|
+ assert entry is not None
|
|
+ assert entry['config'] == ''
|
|
+
|
|
+
|
|
+def test_save_empty_actor_config():
|
|
+ hash_id = hashlib.sha256('test-empty-actor-config'.encode('utf-8')).hexdigest()
|
|
+ acd = ActorConfigData(hash_id=hash_id, config='')
|
|
+ ac = ActorConfig(
|
|
+ context=_CONTEXT_NAME,
|
|
+ config=acd,
|
|
+ )
|
|
+ ac.store()
|
|
+
|
|
+ assert ac.actor_config_id
|
|
+
|
|
+ entry = None
|
|
+ with get_connection(None) as conn:
|
|
+ cursor = conn.execute('SELECT * FROM actor_config WHERE id = ?;', (ac.actor_config_id,))
|
|
+ cursor.row_factory = dict_factory
|
|
+ entry = cursor.fetchone()
|
|
+
|
|
+ assert entry is not None
|
|
+ assert entry['context'] == _CONTEXT_NAME
|
|
+ assert entry['actor_config_hash'] == hash_id
|
|
+
|
|
+
|
|
+def test_store_actor_config():
|
|
+ hash_id = hashlib.sha256('test-actor-config'.encode('utf-8')).hexdigest()
|
|
+ config_str = json.dumps(_TEST_ACTOR_CONFIG, sort_keys=True)
|
|
+ acd = ActorConfigData(hash_id=hash_id, config=config_str)
|
|
+ ac = ActorConfig(
|
|
+ context=_CONTEXT_NAME,
|
|
+ config=acd,
|
|
+ )
|
|
+ ac.store()
|
|
+
|
|
+ assert ac.actor_config_id
|
|
+
|
|
+ entry = None
|
|
+ with get_connection(None) as conn:
|
|
+ cursor = conn.execute(
|
|
+ 'SELECT * FROM actor_config WHERE id = ?;', (ac.actor_config_id,))
|
|
+ cursor = conn.execute(
|
|
+ 'SELECT * '
|
|
+ 'FROM actor_config '
|
|
+ 'JOIN actor_config_data '
|
|
+ 'ON actor_config.actor_config_hash = actor_config_data.hash '
|
|
+ 'WHERE actor_config.id = ?;',
|
|
+ (ac.actor_config_id,)
|
|
+ )
|
|
+ cursor.row_factory = dict_factory
|
|
+ entry = cursor.fetchone()
|
|
+
|
|
+ assert entry is not None
|
|
+ assert entry['context'] == _CONTEXT_NAME
|
|
+ assert entry['actor_config_hash'] == hash_id
|
|
+ ans_config = json.loads(entry['config'])
|
|
+ assert ans_config == {}
|
|
diff --git a/tests/scripts/test_metadata.py b/tests/scripts/test_metadata.py
|
|
index 9b5c07f..6631c49 100644
|
|
--- a/tests/scripts/test_metadata.py
|
|
+++ b/tests/scripts/test_metadata.py
|
|
@@ -20,7 +20,8 @@ _PHASE_NAME = 'test-phase-name'
|
|
_DIALOG_SCOPE = 'test-dialog'
|
|
|
|
_WORKFLOW_METADATA_FIELDS = ('description', 'name', 'phases', 'short_name', 'tag')
|
|
-_ACTOR_METADATA_FIELDS = ('class_name', 'name', 'description', 'phase', 'tags', 'consumes', 'produces', 'path')
|
|
+_ACTOR_METADATA_FIELDS = ('class_name', 'config_schemas', 'name', 'description',
|
|
+ 'phase', 'tags', 'consumes', 'produces', 'path')
|
|
|
|
_TEST_WORKFLOW_METADATA = {
|
|
'description': 'No description has been provided for the UnitTest workflow.',
|
|
@@ -144,7 +145,7 @@ def test_store_actor_metadata(monkeypatch, repository_dir):
|
|
# ---
|
|
with repository_dir.as_cwd():
|
|
logger = logging.getLogger('leapp.actor.test')
|
|
- with mock.patch.object(logger, 'log') as log_mock:
|
|
+ with mock.patch.object(logger, 'log') as log_mock, mock.patch('os.chdir'):
|
|
definition = ActorDefinition('actors/test', '.', log=log_mock)
|
|
with mock.patch('leapp.repository.actor_definition.get_actor_metadata', return_value=_TEST_ACTOR_METADATA):
|
|
with mock.patch('leapp.repository.actor_definition.get_actors', return_value=[True]):
|
|
diff --git a/tests/scripts/test_repository_actor_definition.py b/tests/scripts/test_repository_actor_definition.py
|
|
index 3fcf757..954928f 100644
|
|
--- a/tests/scripts/test_repository_actor_definition.py
|
|
+++ b/tests/scripts/test_repository_actor_definition.py
|
|
@@ -13,6 +13,7 @@ _FAKE_META_DATA = {
|
|
'name': 'fake-actor',
|
|
'path': 'actors/test',
|
|
'tags': (),
|
|
+ 'config_schemas': (),
|
|
'consumes': (),
|
|
'produces': (),
|
|
'dialogs': (),
|
|
@@ -21,7 +22,7 @@ _FAKE_META_DATA = {
|
|
|
|
|
|
def test_actor_definition(repository_dir):
|
|
- with repository_dir.as_cwd():
|
|
+ with repository_dir.as_cwd(), mock.patch('os.chdir', return_value=None):
|
|
logger = logging.getLogger('leapp.actor.test')
|
|
with mock.patch.object(logger, 'log') as log_mock:
|
|
definition = ActorDefinition('actors/test', '.', log=log_mock)
|
|
@@ -40,6 +41,7 @@ def test_actor_definition(repository_dir):
|
|
assert definition.consumes == _FAKE_META_DATA['consumes']
|
|
assert definition.produces == _FAKE_META_DATA['produces']
|
|
assert definition.tags == _FAKE_META_DATA['tags']
|
|
+ assert definition.config_schemas == _FAKE_META_DATA['config_schemas']
|
|
assert definition.class_name == _FAKE_META_DATA['class_name']
|
|
assert definition.dialogs == _FAKE_META_DATA['dialogs']
|
|
assert definition.name == _FAKE_META_DATA['name']
|
|
@@ -52,11 +54,13 @@ def test_actor_definition(repository_dir):
|
|
assert dumped.pop('produces') == definition.produces
|
|
assert dumped.pop('apis') == definition.apis
|
|
assert dumped.pop('tags') == definition.tags
|
|
+ assert dumped.pop('config_schemas') == definition.config_schemas
|
|
assert dumped.pop('dialogs') == [dialog.serialize() for dialog in definition.dialogs]
|
|
assert dumped.pop('path') == _FAKE_META_DATA['path']
|
|
assert dumped.pop('name') == definition.name
|
|
assert dumped.pop('files') == ('.',)
|
|
assert dumped.pop('libraries') == ('.',)
|
|
+ assert dumped.pop('configs') == ('.',)
|
|
assert dumped.pop('tests') == ('.',)
|
|
assert dumped.pop('tools') == ('.',)
|
|
# Assert to ensure we covered all keys
|
|
--
|
|
2.47.0
|
|
|