mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-15 15:05:48 +00:00
expose trigger configuration schema
Note that this commit contains the following breaking changes: * remote pull and remote push triggers are now enabled by default (with empty target list) * remote pull and remote push triggers now require target option to be set (old behaviour had fallback on `gitremote`) * validation is now considered to be stable, so it is enabled by default in docker image (can be disabled however)
This commit is contained in:
@ -48,6 +48,6 @@ class Triggers(Handler):
|
||||
application = Application(architecture, configuration, report=report, unsafe=unsafe)
|
||||
if args.trigger:
|
||||
loader = application.repository.triggers
|
||||
loader.triggers = [loader.load_trigger(trigger) for trigger in args.trigger]
|
||||
loader.triggers = [loader.load_trigger(trigger, architecture, configuration) for trigger in args.trigger]
|
||||
application.on_start()
|
||||
application.on_result(Result())
|
||||
|
@ -20,16 +20,15 @@
|
||||
import argparse
|
||||
import copy
|
||||
|
||||
from typing import Any, Callable, Dict, Optional, Type
|
||||
from typing import Any, Dict, Type
|
||||
|
||||
from ahriman.application.handlers import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.configuration.schema import CONFIGURATION_SCHEMA, \
|
||||
GITREMOTE_REMOTE_PULL_SCHEMA, GITREMOTE_REMOTE_PUSH_SCHEMA, \
|
||||
REPORT_CONSOLE_SCHEMA, REPORT_EMAIL_SCHEMA, REPORT_HTML_SCHEMA, REPORT_TELEGRAM_SCHEMA,\
|
||||
UPLOAD_GITHUB_SCHEMA, UPLOAD_RSYNC_SCHEMA, UPLOAD_S3_SCHEMA
|
||||
from ahriman.core.configuration.schema import CONFIGURATION_SCHEMA, ConfigurationSchema
|
||||
from ahriman.core.configuration.validator import Validator
|
||||
from ahriman.core.exceptions import ExtensionError
|
||||
from ahriman.core.formatters import ValidationPrinter
|
||||
from ahriman.core.triggers import TriggerLoader
|
||||
|
||||
|
||||
class Validate(Handler):
|
||||
@ -64,7 +63,7 @@ class Validate(Handler):
|
||||
Validate.check_if_empty(args.exit_code, True)
|
||||
|
||||
@staticmethod
|
||||
def schema(architecture: str, configuration: Configuration) -> Dict[str, Any]:
|
||||
def schema(architecture: str, configuration: Configuration) -> ConfigurationSchema:
|
||||
"""
|
||||
get schema with triggers
|
||||
|
||||
@ -73,45 +72,39 @@ class Validate(Handler):
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: configuration validation schema
|
||||
ConfigurationSchema: configuration validation schema
|
||||
"""
|
||||
root = copy.deepcopy(CONFIGURATION_SCHEMA)
|
||||
|
||||
# that's actually bad design, but in order to validate built-in triggers we need to know which are set
|
||||
Validate.schema_insert(architecture, configuration, root, "remote-pull", lambda _: GITREMOTE_REMOTE_PULL_SCHEMA)
|
||||
Validate.schema_insert(architecture, configuration, root, "remote-push", lambda _: GITREMOTE_REMOTE_PUSH_SCHEMA)
|
||||
# create trigger loader instance
|
||||
loader = TriggerLoader()
|
||||
for trigger in loader.selected_triggers(configuration):
|
||||
try:
|
||||
trigger_class = loader.load_trigger_class(trigger)
|
||||
except ExtensionError:
|
||||
continue
|
||||
|
||||
report_schemas = {
|
||||
"console": REPORT_CONSOLE_SCHEMA,
|
||||
"email": REPORT_EMAIL_SCHEMA,
|
||||
"html": REPORT_HTML_SCHEMA,
|
||||
"telegram": REPORT_TELEGRAM_SCHEMA,
|
||||
}
|
||||
for schema_name, schema in report_schemas.items():
|
||||
root[schema_name] = Validate.schema_erase_required(copy.deepcopy(schema))
|
||||
Validate.schema_insert(architecture, configuration, root, "report", report_schemas.get)
|
||||
# default settings if any
|
||||
for schema_name, schema in trigger_class.configuration_schema(architecture, None).items():
|
||||
erased = Validate.schema_erase_required(copy.deepcopy(schema))
|
||||
root[schema_name] = Validate.schema_merge(root.get(schema_name, {}), erased)
|
||||
|
||||
upload_schemas = {
|
||||
"github": UPLOAD_GITHUB_SCHEMA,
|
||||
"rsync": UPLOAD_RSYNC_SCHEMA,
|
||||
"s3": UPLOAD_S3_SCHEMA,
|
||||
}
|
||||
for schema_name, schema in upload_schemas.items():
|
||||
root[schema_name] = Validate.schema_erase_required(copy.deepcopy(schema))
|
||||
Validate.schema_insert(architecture, configuration, root, "upload", upload_schemas.get)
|
||||
# settings according to enabled triggers
|
||||
for schema_name, schema in trigger_class.configuration_schema(architecture, configuration).items():
|
||||
root[schema_name] = Validate.schema_merge(root.get(schema_name, {}), copy.deepcopy(schema))
|
||||
|
||||
return root
|
||||
|
||||
@staticmethod
|
||||
def schema_erase_required(schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def schema_erase_required(schema: ConfigurationSchema) -> ConfigurationSchema:
|
||||
"""
|
||||
recursively remove required field from supplied cerberus schema
|
||||
|
||||
Args:
|
||||
schema(Dict[str, Any]): source schema from which required field must be removed
|
||||
schema(ConfigurationSchema): source schema from which required field must be removed
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: schema without required fields
|
||||
ConfigurationSchema: schema without required fields. Note, that source schema will be modified in-place
|
||||
"""
|
||||
schema.pop("required", None)
|
||||
for value in filter(lambda v: isinstance(v, dict), schema.values()):
|
||||
@ -119,32 +112,24 @@ class Validate(Handler):
|
||||
return schema
|
||||
|
||||
@staticmethod
|
||||
def schema_insert(architecture: str, configuration: Configuration, root: Dict[str, Any], root_section: str,
|
||||
schema_mapping: Callable[[str], Optional[Dict[str, Any]]]) -> Dict[str, Any]:
|
||||
def schema_merge(source: Dict[str, Any], schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
insert child schema into the root schema based on mapping rules
|
||||
|
||||
Notes:
|
||||
Actually it is a bad design, because we are reading triggers configuration from parsers which (basically)
|
||||
don't know anything about triggers. But in order to validate built-in triggers we need to know which are set
|
||||
merge child schema into source. In case if source already contains values, new keys will be added
|
||||
(possibly with overrides - in case if such key already set also)
|
||||
|
||||
Args:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
root(Dict[str, Any]): root schema in which child schema will be inserted
|
||||
root_section(str): section name in root schema
|
||||
schema_mapping(Callable[[str], Optional[Dict[str, Any]]]): extractor for child schema based on trigger type
|
||||
source(Dict[str, Any]): source (current) schema into which will be merged
|
||||
schema(Dict[str, Any]): new schema to be merged
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: modified root schema. Note, however, that schema will be modified in place
|
||||
Dict[str, Any]: schema with added elements from source schema if they were set before and not presented
|
||||
in the new one. Note, that schema will be modified in-place
|
||||
"""
|
||||
if not configuration.has_section(root_section):
|
||||
return root
|
||||
for key, value in source.items():
|
||||
if key not in schema:
|
||||
schema[key] = value # new key found, just add it as is
|
||||
elif isinstance(value, dict):
|
||||
# value is dictionary, so we need to go deeper
|
||||
Validate.schema_merge(value, schema[key])
|
||||
|
||||
targets = configuration.getlist(root_section, "target", fallback=[])
|
||||
for target in targets:
|
||||
section, schema_name = configuration.gettype(target, architecture)
|
||||
if (schema := schema_mapping(schema_name)) is not None:
|
||||
root[section] = copy.deepcopy(schema)
|
||||
|
||||
return root
|
||||
return schema
|
||||
|
@ -189,7 +189,7 @@ class Configuration(configparser.RawConfigParser):
|
||||
|
||||
def getpath(self, *args: Any, **kwargs: Any) -> Path: ... # type: ignore
|
||||
|
||||
def gettype(self, section: str, architecture: str) -> Tuple[str, str]:
|
||||
def gettype(self, section: str, architecture: str, *, fallback: Optional[str] = None) -> Tuple[str, str]:
|
||||
"""
|
||||
get type variable with fallback to old logic. Despite the fact that it has same semantics as other get* methods,
|
||||
but it has different argument list
|
||||
@ -197,6 +197,8 @@ class Configuration(configparser.RawConfigParser):
|
||||
Args:
|
||||
section(str): section name
|
||||
architecture(str): repository architecture
|
||||
fallback(Optional[str], optional): optional fallback type if any. If set, second element of the tuple will
|
||||
be always set to this value (Default value = None)
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: section name and found type name
|
||||
@ -204,9 +206,8 @@ class Configuration(configparser.RawConfigParser):
|
||||
Raises:
|
||||
configparser.NoSectionError: in case if no section found
|
||||
"""
|
||||
group_type = self.get(section, "type", fallback=None) # new-style logic
|
||||
if group_type is not None:
|
||||
return section, group_type
|
||||
if (group_type := self.get(section, "type", fallback=fallback)) is not None:
|
||||
return section, group_type # new-style logic
|
||||
# okay lets check for the section with architecture name
|
||||
full_section = self.section_name(section, architecture)
|
||||
if self.has_section(full_section):
|
||||
@ -274,14 +275,14 @@ class Configuration(configparser.RawConfigParser):
|
||||
self.load(path)
|
||||
self.merge_sections(architecture)
|
||||
|
||||
def set_option(self, section: str, option: str, value: Optional[str]) -> None:
|
||||
def set_option(self, section: str, option: str, value: str) -> None:
|
||||
"""
|
||||
set option. Unlike default ``configparser.RawConfigParser.set`` it also creates section if it does not exist
|
||||
|
||||
Args:
|
||||
section(str): section name
|
||||
option(str): option name
|
||||
value(Optional[str]): option value as string in parsable format
|
||||
value(str): option value as string in parsable format
|
||||
"""
|
||||
if not self.has_section(section):
|
||||
self.add_section(section)
|
||||
|
@ -17,16 +17,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# pylint: disable=too-many-lines
|
||||
__all__ = [
|
||||
"CONFIGURATION_SCHEMA",
|
||||
"GITREMOTE_REMOTE_PULL_SCHEMA", "GITREMOTE_REMOTE_PUSH_SCHEMA",
|
||||
"REPORT_CONSOLE_SCHEMA", "REPORT_EMAIL_SCHEMA", "REPORT_HTML_SCHEMA", "REPORT_TELEGRAM_SCHEMA",
|
||||
"UPLOAD_GITHUB_SCHEMA", "UPLOAD_RSYNC_SCHEMA", "UPLOAD_S3_SCHEMA",
|
||||
]
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
__all__ = ["CONFIGURATION_SCHEMA", "ConfigurationSchema"]
|
||||
|
||||
|
||||
ConfigurationSchema = Dict[str, Dict[str, Any]]
|
||||
|
||||
|
||||
CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"settings": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
@ -292,263 +292,3 @@ CONFIGURATION_SCHEMA = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
GITREMOTE_REMOTE_PULL_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"pull_url": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"pull_branch": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
GITREMOTE_REMOTE_PUSH_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"commit_author": {
|
||||
"type": "string",
|
||||
},
|
||||
"push_url": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"push_branch": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
REPORT_CONSOLE_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["console"],
|
||||
},
|
||||
"use_utf": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
REPORT_EMAIL_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["email"],
|
||||
},
|
||||
"full_template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"path_exists": True,
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"no_empty_report": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"required": True,
|
||||
},
|
||||
"receivers": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"sender": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"ssl": {
|
||||
"type": "string",
|
||||
"allowed": ["ssl", "starttls", "disabled"],
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
REPORT_HTML_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["html"],
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
REPORT_TELEGRAM_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["telegram"],
|
||||
},
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"chat_id": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
"template_type": {
|
||||
"type": "string",
|
||||
"allowed": ["MarkdownV2", "HTML", "Markdown"],
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
UPLOAD_GITHUB_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["github"],
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
UPLOAD_RSYNC_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["rsync"],
|
||||
},
|
||||
"command": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"remote": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
UPLOAD_S3_SCHEMA = {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["s3"],
|
||||
},
|
||||
"access_key": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"bucket": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"chunk_size": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"secret_key": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ class Validator(RootValidator): # type: ignore
|
||||
Returns:
|
||||
int: value converted to int according to configuration rules
|
||||
"""
|
||||
del self
|
||||
return int(value)
|
||||
|
||||
def _normalize_coerce_list(self, value: str) -> List[str]:
|
||||
|
@ -17,6 +17,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import List, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.gitremote.remote_pull import RemotePull
|
||||
from ahriman.core.triggers import Trigger
|
||||
@ -30,6 +32,22 @@ class RemotePullTrigger(Trigger):
|
||||
targets(List[str]): git remote target list
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"gitremote": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"pull_url": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"pull_branch": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
CONFIGURATION_SCHEMA_FALLBACK = "gitremote"
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
@ -39,13 +57,27 @@ class RemotePullTrigger(Trigger):
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, architecture, configuration)
|
||||
self.targets = configuration.getlist("remote-pull", "target", fallback=["gitremote"])
|
||||
self.targets = self.configuration_sections(configuration)
|
||||
|
||||
@classmethod
|
||||
def configuration_sections(cls: Type[Trigger], configuration: Configuration) -> List[str]:
|
||||
"""
|
||||
extract configuration sections from configuration
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
List[str]: read configuration sections belong to this trigger
|
||||
"""
|
||||
return configuration.getlist("remote-pull", "target", fallback=[])
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
trigger action which will be called at the start of the application
|
||||
"""
|
||||
for target in self.targets:
|
||||
section, _ = self.configuration.gettype(target, self.architecture)
|
||||
section, _ = self.configuration.gettype(
|
||||
target, self.architecture, fallback=self.CONFIGURATION_SCHEMA_FALLBACK)
|
||||
runner = RemotePull(self.configuration, section)
|
||||
runner.run()
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import Iterable
|
||||
from typing import Iterable, List, Type
|
||||
|
||||
from ahriman.core import context
|
||||
from ahriman.core.configuration import Configuration
|
||||
@ -37,6 +37,25 @@ class RemotePushTrigger(Trigger):
|
||||
targets(List[str]): git remote target list
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"gitremote": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"commit_author": {
|
||||
"type": "string",
|
||||
},
|
||||
"push_url": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"push_branch": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
CONFIGURATION_SCHEMA_FALLBACK = "gitremote"
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
@ -46,7 +65,20 @@ class RemotePushTrigger(Trigger):
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, architecture, configuration)
|
||||
self.targets = configuration.getlist("remote-push", "target", fallback=["gitremote"])
|
||||
self.targets = self.configuration_sections(configuration)
|
||||
|
||||
@classmethod
|
||||
def configuration_sections(cls: Type[Trigger], configuration: Configuration) -> List[str]:
|
||||
"""
|
||||
extract configuration sections from configuration
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
List[str]: read configuration sections belong to this trigger
|
||||
"""
|
||||
return configuration.getlist("remote-push", "target", fallback=[])
|
||||
|
||||
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
@ -63,6 +95,7 @@ class RemotePushTrigger(Trigger):
|
||||
database = ctx.get(ContextKey("database", SQLite))
|
||||
|
||||
for target in self.targets:
|
||||
section, _ = self.configuration.gettype(target, self.architecture)
|
||||
section, _ = self.configuration.gettype(
|
||||
target, self.architecture, fallback=self.CONFIGURATION_SCHEMA_FALLBACK)
|
||||
runner = RemotePush(self.configuration, database, section)
|
||||
runner.run(result)
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import Iterable
|
||||
from typing import Iterable, List, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.triggers import Trigger
|
||||
@ -34,6 +34,148 @@ class ReportTrigger(Trigger):
|
||||
targets(List[str]): report target list
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"console": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["console"],
|
||||
},
|
||||
"use_utf": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
"email": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["email"],
|
||||
},
|
||||
"full_template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"path_exists": True,
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"no_empty_report": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"required": True,
|
||||
},
|
||||
"receivers": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"sender": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"ssl": {
|
||||
"type": "string",
|
||||
"allowed": ["ssl", "starttls", "disabled"],
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
"user": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"html": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["html"],
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
"telegram": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["telegram"],
|
||||
},
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"chat_id": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
},
|
||||
"link_path": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"template_path": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
"required": True,
|
||||
"path_exists": True,
|
||||
},
|
||||
"template_type": {
|
||||
"type": "string",
|
||||
"allowed": ["MarkdownV2", "HTML", "Markdown"],
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
@ -43,7 +185,20 @@ class ReportTrigger(Trigger):
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, architecture, configuration)
|
||||
self.targets = configuration.getlist("report", "target", fallback=[])
|
||||
self.targets = self.configuration_sections(configuration)
|
||||
|
||||
@classmethod
|
||||
def configuration_sections(cls: Type[Trigger], configuration: Configuration) -> List[str]:
|
||||
"""
|
||||
extract configuration sections from configuration
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
List[str]: read configuration sections belong to this trigger
|
||||
"""
|
||||
return configuration.getlist("report", "target", fallback=[])
|
||||
|
||||
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
|
@ -80,4 +80,4 @@ class RepositoryProperties(LazyLogging):
|
||||
self.sign = GPG(architecture, configuration)
|
||||
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
|
||||
self.reporter = Client.load(configuration, report=report)
|
||||
self.triggers = TriggerLoader(architecture, configuration)
|
||||
self.triggers = TriggerLoader.load(architecture, configuration)
|
||||
|
@ -17,9 +17,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import Iterable
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterable, List, Optional, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.configuration.schema import ConfigurationSchema
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
@ -30,6 +33,9 @@ class Trigger(LazyLogging):
|
||||
trigger base class
|
||||
|
||||
Attributes:
|
||||
CONFIGURATION_SCHEMA(ConfigurationSchema): (class attribute) configuration schema template
|
||||
CONFIGURATION_SCHEMA_FALLBACK(Optional[str]): (class attribute) optional fallback option for defining
|
||||
configuration schema type used
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
@ -47,10 +53,13 @@ class Trigger(LazyLogging):
|
||||
>>> configuration = Configuration()
|
||||
>>> configuration.set_option("build", "triggers", "my.awesome.package.CustomTrigger")
|
||||
>>>
|
||||
>>> loader = TriggerLoader("x86_64", configuration)
|
||||
>>> loader = TriggerLoader.load("x86_64", configuration)
|
||||
>>> loader.on_result(Result(), [])
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA: ConfigurationSchema = {}
|
||||
CONFIGURATION_SCHEMA_FALLBACK: Optional[str] = None
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
@ -62,6 +71,60 @@ class Trigger(LazyLogging):
|
||||
self.architecture = architecture
|
||||
self.configuration = configuration
|
||||
|
||||
@classmethod
|
||||
def configuration_schema(cls: Type[Trigger], architecture: str,
|
||||
configuration: Optional[Configuration]) -> ConfigurationSchema:
|
||||
"""
|
||||
configuration schema based on supplied service configuration
|
||||
|
||||
Notes:
|
||||
Schema must be in cerberus format, for details and examples you can check built-in triggers.
|
||||
|
||||
Args:
|
||||
architecture(str): repository architecture
|
||||
configuration(Optional[Configuration]): configuration instance. If set to None, the default schema
|
||||
should be returned
|
||||
|
||||
Returns:
|
||||
ConfigurationSchema: configuration schema in cerberus format
|
||||
"""
|
||||
if configuration is None:
|
||||
return cls.CONFIGURATION_SCHEMA
|
||||
|
||||
result: ConfigurationSchema = {}
|
||||
for target in cls.configuration_sections(configuration):
|
||||
if not configuration.has_section(target):
|
||||
continue
|
||||
section, schema_name = configuration.gettype(
|
||||
target, architecture, fallback=cls.CONFIGURATION_SCHEMA_FALLBACK)
|
||||
if schema_name not in cls.CONFIGURATION_SCHEMA:
|
||||
continue
|
||||
result[section] = cls.CONFIGURATION_SCHEMA[schema_name]
|
||||
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def configuration_sections(cls: Type[Trigger], configuration: Configuration) -> List[str]:
|
||||
"""
|
||||
extract configuration sections from configuration
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
List[str]: read configuration sections belong to this trigger
|
||||
|
||||
Examples:
|
||||
This method can be used in order to extract specific configuration sections which are set by user, e.g.
|
||||
from sources::
|
||||
|
||||
>>> @staticmethod
|
||||
>>> def configuration_sections(cls: Type[Trigger], configuration: Configuration) -> List[str]:
|
||||
>>> return configuration.getlist("report", "target", fallback=[])
|
||||
"""
|
||||
del configuration
|
||||
return []
|
||||
|
||||
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
trigger action which will be called after build process with process result
|
||||
|
@ -17,13 +17,15 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import importlib
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Generator, Iterable
|
||||
from typing import Generator, Iterable, List, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import ExtensionError
|
||||
@ -38,8 +40,6 @@ class TriggerLoader(LazyLogging):
|
||||
trigger loader class
|
||||
|
||||
Attributes:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
triggers(List[Trigger]): list of loaded triggers according to the configuration
|
||||
|
||||
Examples:
|
||||
@ -50,7 +50,7 @@ class TriggerLoader(LazyLogging):
|
||||
|
||||
Having such configuration you can create instance of the loader::
|
||||
|
||||
>>> loader = TriggerLoader("x86_64", configuration)
|
||||
>>> loader = TriggerLoader.load("x86_64", configuration)
|
||||
>>> print(loader.triggers)
|
||||
|
||||
After that you are free to run triggers::
|
||||
@ -58,23 +58,46 @@ class TriggerLoader(LazyLogging):
|
||||
>>> loader.on_result(Result(), [])
|
||||
"""
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
default constructor
|
||||
"""
|
||||
self._on_stop_requested = False
|
||||
self.triggers: List[Trigger] = []
|
||||
|
||||
@classmethod
|
||||
def load(cls: Type[TriggerLoader], architecture: str, configuration: Configuration) -> TriggerLoader:
|
||||
"""
|
||||
create instance from configuration
|
||||
|
||||
Args:
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
self.architecture = architecture
|
||||
self.configuration = configuration
|
||||
|
||||
self._on_stop_requested = False
|
||||
self.triggers = [
|
||||
self.load_trigger(trigger)
|
||||
for trigger in configuration.getlist("build", "triggers")
|
||||
Returns:
|
||||
TriggerLoader: fully loaded trigger instance
|
||||
"""
|
||||
instance = cls()
|
||||
instance.triggers = [
|
||||
instance.load_trigger(trigger, architecture, configuration)
|
||||
for trigger in instance.selected_triggers(configuration)
|
||||
]
|
||||
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def selected_triggers(configuration: Configuration) -> List[str]:
|
||||
"""
|
||||
read configuration and return triggers which are set by settings
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
List[str]: list of triggers according to configuration
|
||||
"""
|
||||
return configuration.getlist("build", "triggers", fallback=[])
|
||||
|
||||
@contextlib.contextmanager
|
||||
def __execute_trigger(self, trigger: Trigger) -> Generator[None, None, None]:
|
||||
"""
|
||||
@ -130,16 +153,39 @@ class TriggerLoader(LazyLogging):
|
||||
except ModuleNotFoundError:
|
||||
raise ExtensionError(f"Module {package} not found")
|
||||
|
||||
def load_trigger(self, module_path: str) -> Trigger:
|
||||
def load_trigger(self, module_path: str, architecture: str, configuration: Configuration) -> Trigger:
|
||||
"""
|
||||
load trigger by module path
|
||||
|
||||
Args:
|
||||
module_path(str): module import path to load
|
||||
architecture(str): repository architecture
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
Trigger: loaded trigger based on settings
|
||||
|
||||
Raises:
|
||||
InvalidExtension: in case if trigger could not be instantiated
|
||||
"""
|
||||
trigger_type = self.load_trigger_class(module_path)
|
||||
try:
|
||||
trigger = trigger_type(architecture, configuration)
|
||||
except Exception:
|
||||
raise ExtensionError(f"Could not load instance of trigger from {trigger_type} loaded from {module_path}")
|
||||
|
||||
return trigger
|
||||
|
||||
def load_trigger_class(self, module_path: str) -> Type[Trigger]:
|
||||
"""
|
||||
load trigger class by module path
|
||||
|
||||
Args:
|
||||
module_path(str): module import path to load
|
||||
|
||||
Returns:
|
||||
Type[Trigger]: loaded trigger type by module path
|
||||
|
||||
Raises:
|
||||
InvalidExtension: in case if module cannot be loaded from the specified module path or is not a trigger
|
||||
"""
|
||||
@ -156,16 +202,11 @@ class TriggerLoader(LazyLogging):
|
||||
trigger_type = getattr(module, class_name, None)
|
||||
if not isinstance(trigger_type, type):
|
||||
raise ExtensionError(f"{class_name} of {package_or_path} is not a type")
|
||||
if not issubclass(trigger_type, Trigger):
|
||||
raise ExtensionError(f"Class {class_name} of {package_or_path} is not a Trigger subclass")
|
||||
|
||||
self.logger.info("loaded type %s of package %s", class_name, package_or_path)
|
||||
|
||||
try:
|
||||
trigger = trigger_type(self.architecture, self.configuration)
|
||||
except Exception:
|
||||
raise ExtensionError(f"Could not load instance of trigger from {class_name} of {package_or_path}")
|
||||
if not isinstance(trigger, Trigger):
|
||||
raise ExtensionError(f"Class {class_name} of {package_or_path} is not a Trigger")
|
||||
|
||||
return trigger
|
||||
return trigger_type
|
||||
|
||||
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import Iterable
|
||||
from typing import Iterable, List, Type
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.triggers import Trigger
|
||||
@ -34,6 +34,86 @@ class UploadTrigger(Trigger):
|
||||
targets(List[str]): upload target list
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"github": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["github"],
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
"username": {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
},
|
||||
"rsync": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["rsync"],
|
||||
},
|
||||
"command": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
"schema": {"type": "string"},
|
||||
"required": True,
|
||||
"empty": False,
|
||||
},
|
||||
"remote": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
"s3": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"allowed": ["s3"],
|
||||
},
|
||||
"access_key": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"bucket": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"chunk_size": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
"secret_key": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, architecture: str, configuration: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
@ -43,7 +123,20 @@ class UploadTrigger(Trigger):
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, architecture, configuration)
|
||||
self.targets = configuration.getlist("upload", "target", fallback=[])
|
||||
self.targets = self.configuration_sections(configuration)
|
||||
|
||||
@classmethod
|
||||
def configuration_sections(cls: Type[Trigger], configuration: Configuration) -> List[str]:
|
||||
"""
|
||||
extract configuration sections from configuration
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
List[str]: read configuration sections belong to this trigger
|
||||
"""
|
||||
return configuration.getlist("upload", "target", fallback=[])
|
||||
|
||||
def on_result(self, result: Result, packages: Iterable[Package]) -> None:
|
||||
"""
|
||||
|
Reference in New Issue
Block a user