Skip to content

Commit

Permalink
Implement and Enhance Actor Configuration Support
Browse files Browse the repository at this point in the history
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 `leappdb`.

- **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.

JIRA: OAMG-8803

Co-authored-by: David Kubek <dkubek@example.com>
  • Loading branch information
2 people authored and dkubek committed Oct 14, 2024
1 parent 9787116 commit e2f13b4
Show file tree
Hide file tree
Showing 27 changed files with 724 additions and 95 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions etc/leapp/leapp.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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/
80 changes: 48 additions & 32 deletions leapp/actors/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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>`
Expand Down Expand Up @@ -86,6 +100,7 @@ def serialize(self):
'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],
Expand All @@ -100,15 +115,20 @@ def __init__(self, messaging=None, logger=None, config_model=None, skip_dialogs=
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
self.log = (logger or logging.getLogger('leapp.actors')).getChild(self.name)
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

Expand Down Expand Up @@ -359,6 +379,17 @@ def report_error(self, message, severity=ErrorSeverity.ERROR, details=None):
actor=self,
details=details)

def retrieve_config(self):
"""
Retrieve the configuration specific to the specified schema.
:param schema: Configuration schemas
:type schema: :py:class:`leapp.models.Model`
:return: Dictionary containing requested configuration.
:rtype: dict
"""
return retrieve_config(self.config_schema)


def _is_type(value_type):
def validate(actor, name, value):
Expand Down Expand Up @@ -390,17 +421,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([True] + [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")
Expand All @@ -412,28 +449,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)
Expand Down Expand Up @@ -464,13 +479,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=())
])


Expand Down
Loading

0 comments on commit e2f13b4

Please sign in to comment.