Skip to content
This repository was archived by the owner on Jan 14, 2024. It is now read-only.

Commit

Permalink
#66: WIP - implement configure() via ConfigurationLifecycleEvent
Browse files Browse the repository at this point in the history
…+ :docker:exec task as an example
blackandred committed Jun 28, 2021
1 parent f4a5f6e commit 9e0b99c
Showing 5 changed files with 217 additions and 18 deletions.
12 changes: 12 additions & 0 deletions src/core/rkd/core/api/lifecycle.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
Lifecycle contract
==================
"""
from typing import List


class CompilationLifecycleEventAware(object):
@@ -10,3 +11,14 @@ def compile(self, event: 'CompilationLifecycleEvent') -> None:
Execute code after all tasks were collected into a single context
"""
pass


class ConfigurationLifecycleEventAware(object):
def get_configuration_attributes(self) -> List[str]:
return []

def configure(self, event: 'ConfigurationLifecycleEvent') -> None:
"""
Executes before all tasks are executed. ORDER DOES NOT MATTER, can be executed in parallel.
"""
pass
20 changes: 15 additions & 5 deletions src/core/rkd/core/bootstrap.py
Original file line number Diff line number Diff line change
@@ -11,13 +11,16 @@
import sys
import os
from dotenv import load_dotenv

from rkd.core.execution.lifecycle import ConfigurationResolver
from .execution.results import ProgressObserver
from .argparsing.parser import CommandlineParsingHelper
from .context import ContextFactory, ApplicationContext
from .resolver import TaskResolver
from .validator import TaskDeclarationValidator
from .execution.executor import OneByOneTaskExecutor
from .exception import TaskNotFoundException, ParsingException, YamlParsingException, CommandlineParsingError
from .exception import TaskNotFoundException, ParsingException, YamlParsingException, CommandlineParsingError, \
HandledExitException
from .api.inputoutput import SystemIO
from .api.inputoutput import UnbufferedStdout
from .aliasgroups import parse_alias_groups_from_env
@@ -81,6 +84,7 @@ def main(self, argv: list):
observer = ProgressObserver(io)
task_resolver = TaskResolver(self._ctx, parse_alias_groups_from_env(os.getenv('RKD_ALIAS_GROUPS', '')))
executor = OneByOneTaskExecutor(self._ctx, observer)
config_resolver = ConfigurationResolver(io)

# iterate over each task, parse commandline arguments
try:
@@ -89,11 +93,17 @@ def main(self, argv: list):
io.error_msg(str(err))
sys.exit(1)

# validate all tasks
task_resolver.resolve(requested_tasks, TaskDeclarationValidator.assert_declaration_is_valid)
try:
# validate all tasks
task_resolver.resolve(requested_tasks, TaskDeclarationValidator.assert_declaration_is_valid)

# resolve configuration
task_resolver.resolve(requested_tasks, config_resolver.run_event)

# execute all tasks
task_resolver.resolve(requested_tasks, executor.execute)
# execute all tasks
task_resolver.resolve(requested_tasks, executor.execute)
except HandledExitException:
sys.exit(1)

executor.get_observer().execution_finished()

49 changes: 39 additions & 10 deletions src/core/rkd/core/exception.py
Original file line number Diff line number Diff line change
@@ -3,15 +3,28 @@
from .argparsing.model import TaskArguments


class ContextException(Exception):
class RiotKitDoException(Exception):
pass


class HandledExitException(Exception):
"""
Signal to exit RKD without any message, as the error was already handled
"""


class ContextException(RiotKitDoException):
pass


class TaskNotFoundException(ContextException):
pass


class TaskExecutionException(Exception):
# +=====================
# + EXECUTOR EXCEPTIONS
# +=====================
class TaskExecutionException(RiotKitDoException):
pass


@@ -31,6 +44,9 @@ def __init__(self, args: List[TaskArguments] = None):
self.args = args


# +==================================================
# + EXECUTOR EXCEPTIONS -> Task rescheduling signals
# +==================================================
class ExecutionRescheduleException(TaskExecutionException):
"""Internal signal to put extra task into resolve/schedule queue of TaskResolver"""

@@ -48,29 +64,42 @@ class ExecutionErrorActionException(ExecutionRescheduleException):
"""Internal signal to call an error notification in case when given task fails"""


class TaskException(ContextException):
# +=============================
# + TASK DECLARATION EXCEPTIONS
# -----------------------------
# When inputs are invalid
# +=============================
class TaskDeclarationException(ContextException):
pass


class UndefinedEnvironmentVariableUsageError(TaskException):
class LifecycleConfigurationException(TaskDeclarationException):
@classmethod
def from_invalid_method_used(cls, task_full_name: str, method_name: str) -> 'LifecycleConfigurationException':
return cls(f'Attribute or method "{method_name}" is not allowed ' +
f'to be used in configure() of "{task_full_name}" task. Make sure you are not trying to do ' +
'anything tricky')


class UndefinedEnvironmentVariableUsageError(TaskDeclarationException):
pass


class EnvironmentVariableNotUsed(TaskException):
class EnvironmentVariableNotUsed(TaskDeclarationException):
pass


class EnvironmentVariableNameNotAllowed(TaskException):
class EnvironmentVariableNameNotAllowed(TaskDeclarationException):
def __init__(self, var_name: str):
super().__init__('Environment variable with this name "' + var_name + '" cannot be declared, it probably a' +
' commonly reserved name by operating systems')


class UserInputException(Exception):
class UserInputException(RiotKitDoException):
pass


class BlockDefinitionLogicError(Exception):
class BlockDefinitionLogicError(RiotKitDoException):
@staticmethod
def from_both_rescue_and_error_defined():
return BlockDefinitionLogicError('Block "{0:s}" cannot define both @rescue and @error'.format(task.block().body))
@@ -161,7 +190,7 @@ def __init__(self, path: str, lookup_paths: list):
)


class RuntimeException(Exception):
class RuntimeException(RiotKitDoException):
pass


@@ -196,4 +225,4 @@ def from_block_closing_not_found(pos: int):

@staticmethod
def from_block_ending_not_found(block: str):
return CommandlineParsingError('Parsing exception: Block ending - %s not found' % block)
return CommandlineParsingError('Parsing exception: Block ending - %s not found' % block)
108 changes: 105 additions & 3 deletions src/core/rkd/core/execution/lifecycle.py
Original file line number Diff line number Diff line change
@@ -12,12 +12,15 @@
only allowed methods can be executed.
"""

from inspect import getsource
from copy import copy
from textwrap import dedent
from typing import Dict, Union, List
from rkd.core.api.contract import TaskInterface
from rkd.core.api.lifecycle import CompilationLifecycleEventAware
from rkd.core.api.lifecycle import CompilationLifecycleEventAware, ConfigurationLifecycleEventAware
from rkd.core.api.inputoutput import IO
from rkd.core.api.syntax import TaskDeclaration, GroupDeclaration
from rkd.core.exception import LifecycleConfigurationException, HandledExitException


class CompilationLifecycleEvent(object):
@@ -94,12 +97,111 @@ def run_event(io: IO, compiled: Dict[str, Union[TaskDeclaration, GroupDeclaratio
io.internal(f'compile({declaration})')

if not isinstance(declaration, TaskDeclaration):
io.internal('compile skipped, not a TaskDeclaration')
io.internal('compilation skipped, not a TaskDeclaration')
continue

if not isinstance(declaration.get_task_to_execute(), CompilationLifecycleEventAware):
io.internal('compile skipped, task does not implement CompilationLifecycleEventAware')
io.internal('compilation skipped, task does not implement CompilationLifecycleEventAware')
continue

task: Union[CompilationLifecycleEventAware, TaskInterface] = declaration.get_task_to_execute()
task.compile(CompilationLifecycleEvent(declaration, compiled, io))


class ConfigurationLifecycleEvent(object):
pass


class LimitedScopeTaskProxy(object):
"""
Proxy used to limit operations scope
"""

__task: TaskInterface
__declaration: TaskDeclaration
__allowed: List[str]

def __init__(self, task: TaskInterface, declaration: TaskDeclaration, allowed: List[str]):
self.__task = task
self.__declaration = declaration
self.__allowed = allowed

def __getattr__(self, name: str):
if self.__allowed and name not in self.__allowed:
raise LifecycleConfigurationException.from_invalid_method_used(
task_full_name=self.__declaration.to_full_name(),
method_name=name
)

return self.__task.__getattribute__(name)


class ConfigurationResolver(object):
"""
Goes through task-by-task and calls configure() ONLY WHEN method implements ConfigurationLifecycleEventAware
interface.
NOTICE: Each configure() method gets LimitedScopeTaskProxy() instance as self to limit scope of what can be done
"""

io: IO

def __init__(self, io: IO):
self.io = io

def run_event(self, declaration: TaskDeclaration, task_num: int, parent: Union[GroupDeclaration, None] = None,
args: list = None):

if not isinstance(declaration, TaskDeclaration):
self.io.internal('configuration skipped, not a TaskDeclaration')
return

if not isinstance(declaration.get_task_to_execute(), ConfigurationLifecycleEventAware):
self.io.internal('configuration skipped, task does not implement ConfigurationLifecycleEventAware')
return

self.io.internal(f'configuring {declaration}')

# create a proxy method that will reduce the scope of what can be done in configure() stage
task: Union[ConfigurationLifecycleEventAware, TaskInterface] = declaration.get_task_to_execute()
task.__setattr__('configure',
self.create_proxy_method(
task,
LimitedScopeTaskProxy(
task=task,
allowed=task.get_configuration_attributes(),
declaration=declaration
)))

# call configuration
try:
task.configure(ConfigurationLifecycleEvent())

except Exception as err:
self.io.error_msg(str(err))
self.io.error_msg(f'Task "{declaration}" cannot be configured')

raise HandledExitException() from err

@classmethod
def create_proxy_method(cls, task: ConfigurationLifecycleEventAware, scope: LimitedScopeTaskProxy):
"""
Creates proxy method that will replace `configure()` inside TaskInterface (only for one instance, not for class)
Purpose:
:param task:
:param scope:
:return:
"""

source_code_lines = getsource(task.configure).split("\n")
del source_code_lines[0]
source_code = "# source code generated by proxy method\n" + dedent("\n".join(source_code_lines))

# signature: def configure (self, event: ConfigurationLifecycleEvent)
def _configure_proxy_method(event: ConfigurationLifecycleEvent):
self = scope

return exec(source_code)

return _configure_proxy_method
46 changes: 46 additions & 0 deletions src/core/rkd/core/standardlib/docker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from argparse import ArgumentParser
from typing import List

from rkd.core.api.contract import TaskInterface, ExecutionContext
from rkd.core.api.lifecycle import ConfigurationLifecycleEventAware
from rkd.core.api.syntax import TaskDeclaration
from rkd.core.execution.lifecycle import ConfigurationLifecycleEvent


class RunInContainerTask(TaskInterface, ConfigurationLifecycleEventAware):
"""
Runs a command inside a container
"""

docker_image: str

def get_configuration_attributes(self) -> List[str]:
return [
'docker_image'
]

def configure(self, event: ConfigurationLifecycleEvent) -> None:
self.docker_image = 'test'

print('!!!', self.docker_image)
print('???', self)

print(self.get_name())

def get_name(self) -> str:
return ':exec'

def get_group_name(self) -> str:
return ':docker'

def execute(self, context: ExecutionContext) -> bool:
return True

def configure_argparse(self, parser: ArgumentParser):
pass


def imports() -> list:
return [
TaskDeclaration(RunInContainerTask(), internal=True)
]

0 comments on commit 9e0b99c

Please sign in to comment.