Arguments
This commit is contained in:
parent
33af22443e
commit
8a44efc464
|
@ -24,7 +24,8 @@ def main():
|
||||||
default=False,
|
default=False,
|
||||||
help="Display output for flow",
|
help="Display output for flow",
|
||||||
)
|
)
|
||||||
def execute(defintinion_file, print_output):
|
@click.option("--argument", "-a", "defined_arguments", multiple=True, default=[])
|
||||||
|
def execute(defintinion_file, print_output, defined_arguments=list):
|
||||||
extension = os.path.splitext(defintinion_file.name)[1][1:]
|
extension = os.path.splitext(defintinion_file.name)[1][1:]
|
||||||
|
|
||||||
if not hasattr(FlowParser, f"from_{extension}"):
|
if not hasattr(FlowParser, f"from_{extension}"):
|
||||||
|
@ -35,8 +36,13 @@ def execute(defintinion_file, print_output):
|
||||||
|
|
||||||
flow = getattr(FlowParser, f"from_{extension}")(defintinion_file.read())
|
flow = getattr(FlowParser, f"from_{extension}")(defintinion_file.read())
|
||||||
|
|
||||||
|
arguments = {}
|
||||||
|
for argument in defined_arguments:
|
||||||
|
key, value = argument.split("=")
|
||||||
|
arguments[key] = value
|
||||||
|
|
||||||
title(f"Running flow: {flow.name}")
|
title(f"Running flow: {flow.name}")
|
||||||
executor = Executor(flow)
|
executor = Executor(flow=flow, defined_arguments=arguments)
|
||||||
|
|
||||||
for n, step in enumerate(executor.steps, start=1):
|
for n, step in enumerate(executor.steps, start=1):
|
||||||
click.echo(
|
click.echo(
|
||||||
|
@ -44,12 +50,14 @@ def execute(defintinion_file, print_output):
|
||||||
nl=False,
|
nl=False,
|
||||||
)
|
)
|
||||||
result = executor.execute_step(step)
|
result = executor.execute_step(step)
|
||||||
|
|
||||||
if not result.success:
|
if not result.success:
|
||||||
error(f"Executing step [{n}/{executor.step_count}]: {step.task.name}")
|
error(f"Executing step [{n}/{executor.step_count}]: {step.task.name}")
|
||||||
|
if print_output:
|
||||||
|
click.echo(result.output)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
success(f"Running step [{n}/{executor.step_count}]: {step.task.name}")
|
success(f"Running step [{n}/{executor.step_count}]: {step.task.name}")
|
||||||
|
|
||||||
if print_output:
|
if print_output:
|
||||||
title("Full output:")
|
click.echo(result.output)
|
||||||
click.echo("\n".join(executor._execution.output))
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
|
||||||
|
from jinja2 import Template
|
||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,11 +9,20 @@ class Action:
|
||||||
id = ""
|
id = ""
|
||||||
|
|
||||||
class Parameters(pydantic.BaseModel):
|
class Parameters(pydantic.BaseModel):
|
||||||
pass
|
PARSE_WITH_ARGUMENTS = set()
|
||||||
|
|
||||||
def __init__(self, parameters=None):
|
def __init__(self, parameters=None):
|
||||||
self.logger = logging.getLogger(self.__class__.__name__)
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
self.parameters = self.Parameters(**(parameters or {}))
|
self.parameters = self.Parameters(**(parameters or {}))
|
||||||
|
self.parsed_parameters = {}
|
||||||
|
|
||||||
|
def parse_parameters_with_arguments(self, **arguments):
|
||||||
|
"""
|
||||||
|
Returns a dict with the parameters parsed in base of the provided arguments.
|
||||||
|
Parsing using jinja2 template themes only on the fields defined on the `Parameters.PARSE_WITH_ARGUMENTS`.
|
||||||
|
"""
|
||||||
|
for parameter_name in self.parameters.PARSE_WITH_ARGUMENTS:
|
||||||
|
self.parsed_parameters[parameter_name] = Template(self.parameters.dict()[parameter_name]).render(**arguments)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def execute(self, workspace, **kwargs):
|
def execute(self, workspace, **kwargs):
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from typing import Text
|
import os.path
|
||||||
|
from typing import Text, Optional
|
||||||
|
|
||||||
import docker
|
import docker
|
||||||
import pydantic
|
import pydantic
|
||||||
|
@ -8,57 +9,96 @@ from jeeves.core.objects import Result
|
||||||
from .base import Action
|
from .base import Action
|
||||||
|
|
||||||
|
|
||||||
class DockerAction(Action):
|
class DockerBuildAction(Action):
|
||||||
"""
|
"""
|
||||||
.. automethod:: _run_container
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id = "contrib/docker"
|
id = "contrib/docker/build"
|
||||||
|
verbose_name = "Build docker image"
|
||||||
|
|
||||||
|
class Parameters(pydantic.BaseModel):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
dockerfile_path: Text
|
||||||
|
image_name: Text
|
||||||
|
tag: Text = "latest"
|
||||||
|
|
||||||
|
def execute(self, **kwargs):
|
||||||
|
workspace = kwargs.get("workspace")
|
||||||
|
|
||||||
|
client = docker.from_env()
|
||||||
|
|
||||||
|
client.images.build(
|
||||||
|
path=workspace,
|
||||||
|
fileobj=os.path.join(workspace, self.parameters.dockerfile_path),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DockerRunAction(Action):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = "contrib/docker/run"
|
||||||
verbose_name = "Execute docker container"
|
verbose_name = "Execute docker container"
|
||||||
|
|
||||||
class Parameters(pydantic.BaseModel):
|
class Parameters(pydantic.BaseModel):
|
||||||
"""
|
"""
|
||||||
+----------------+------+-----------+----------------------------------------------+
|
+----------------------+------+-----------+--------------------------------------------------------------+
|
||||||
| Parameter name | Type | Mandatory | Description |
|
| Parameter name | Type | Mandatory | Description |
|
||||||
+================+======+===========+==============================================+
|
+======================+======+===========+==============================================================+
|
||||||
| ``image`` | text | no | Image to run (defaults to ``alpine:latest``) |
|
| ``image`` | text | no | Image to run (defaults to ``alpine:latest``) |
|
||||||
| ``command`` | text | yes | The command to be executed |
|
| ``command`` | text | yes | The command to be executed |
|
||||||
+----------------+------+-----------+----------------------------------------------+
|
| ``entrypoint`` | text | no | The entrypoint to use (defaults in image) |
|
||||||
|
| ``tty`` | bool | no | Allocate pseudo-tty (defaults to ``false``) |
|
||||||
|
| ``remove_container`` | text | no | Select to remove container after run (defaults to ``true`` ) |
|
||||||
|
+----------------------+------+-----------+--------------------------------------------------------------+
|
||||||
"""
|
"""
|
||||||
|
|
||||||
image: Text = "alpine:latest"
|
image: Text = "alpine:latest"
|
||||||
command: Text
|
command: Text
|
||||||
|
entrypoint: Optional[Text] = None
|
||||||
|
tty: bool = False
|
||||||
remove_container: bool = True
|
remove_container: bool = True
|
||||||
|
|
||||||
def _run_container(self):
|
PARSE_WITH_ARGUMENTS = {"command", "image"}
|
||||||
"""
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def execute(self, **kwargs):
|
def execute(self, **kwargs):
|
||||||
workspace = kwargs.get("workspace")
|
workspace = kwargs.get("workspace")
|
||||||
|
arguments = kwargs.get("arguments")
|
||||||
image = self.parameters.image
|
image = self.parameters.image
|
||||||
command = self.parameters.command
|
|
||||||
environment = {"WORKSPACE_PATH": "/workspace"}
|
environment = {"WORKSPACE_PATH": "/workspace"}
|
||||||
|
|
||||||
|
# Add arguments as uppercase environment variables prefixed with JEEVES_
|
||||||
|
if arguments:
|
||||||
|
for key, value in arguments.items():
|
||||||
|
environment[f"JEEVES_{key.upper}"] = value
|
||||||
|
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
|
|
||||||
self.logger.info("Pulling image...")
|
self.logger.info("Pulling image...")
|
||||||
try:
|
try:
|
||||||
client.images.get(image)
|
client.images.get(image)
|
||||||
except docker.errors.ImageNotFound:
|
except docker.errors.ImageNotFound:
|
||||||
self.logger.error("Image does not exist")
|
self.logger.error(f"Image '{image}' does not exist")
|
||||||
return Result(success=False)
|
return Result(success=False)
|
||||||
|
|
||||||
self.logger.info("Execute command in container...")
|
run_kwargs = dict(
|
||||||
container = client.containers.run(
|
|
||||||
image=image,
|
image=image,
|
||||||
command=command,
|
command=self.parsed_parameters["command"],
|
||||||
detach=True,
|
detach=True,
|
||||||
environment=environment,
|
environment=environment,
|
||||||
|
tty=self.parameters.tty,
|
||||||
volumes={"/workspace": {"bind": str(workspace.path), "mode": "rw"}},
|
volumes={"/workspace": {"bind": str(workspace.path), "mode": "rw"}},
|
||||||
|
working_dir="/workspace",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.parameters.entrypoint:
|
||||||
|
run_kwargs["entrypoint"] = self.parameters.entrypoint
|
||||||
|
|
||||||
|
self.logger.info("Execute command in container...")
|
||||||
|
container = client.containers.run(**run_kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = container.wait(timeout=30, condition="not-running")
|
result = container.wait(timeout=30, condition="not-running")
|
||||||
logs = container.logs()
|
logs = container.logs()
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
import traceback
|
import traceback
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from jeeves.core.objects import Flow, Result, Execution, ExecutionStep
|
from jeeves.core.objects import Flow, Result, Execution, ExecutionStep
|
||||||
from jeeves.core.registry import ActionRegistry
|
from jeeves.core.registry import ActionRegistry
|
||||||
|
|
||||||
|
|
||||||
class Executor:
|
class Executor:
|
||||||
def __init__(self, flow: Flow):
|
def __init__(self, flow: Flow, defined_arguments: Dict = None):
|
||||||
|
defined_arguments = defined_arguments or {}
|
||||||
self.step_count = len(flow.tasks)
|
self.step_count = len(flow.tasks)
|
||||||
self._flow: Flow = flow
|
self._flow: Flow = flow
|
||||||
self._execution = Execution(flow=flow, steps=self._get_steps(flow))
|
self._execution = Execution(flow=flow, steps=self._get_steps(flow))
|
||||||
|
self._arguments = {}
|
||||||
|
if self._flow.arguments:
|
||||||
|
for argument in self._flow.arguments:
|
||||||
|
# TODO: What happens if not default?
|
||||||
|
self._arguments[argument.name] = defined_arguments.get(argument.name, argument.default)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def steps(self):
|
def steps(self):
|
||||||
|
@ -22,6 +29,7 @@ class Executor:
|
||||||
def execute_step(self, step: ExecutionStep):
|
def execute_step(self, step: ExecutionStep):
|
||||||
try:
|
try:
|
||||||
action = ActionRegistry.get_action_cls(step.task.type)(parameters=step.task.parameters)
|
action = ActionRegistry.get_action_cls(step.task.type)(parameters=step.task.parameters)
|
||||||
|
action.parse_parameters_with_arguments(**self._arguments)
|
||||||
step.result = action.execute(workspace=self._execution.workspace)
|
step.result = action.execute(workspace=self._execution.workspace)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
# Catch unhandled exceptions, mark the result as unsuccessful
|
# Catch unhandled exceptions, mark the result as unsuccessful
|
||||||
|
|
|
@ -32,6 +32,7 @@ class Argument(BaseObject):
|
||||||
class Flow(BaseObject):
|
class Flow(BaseObject):
|
||||||
name: Text
|
name: Text
|
||||||
tasks: List[Task] = field(default_factory=list)
|
tasks: List[Task] = field(default_factory=list)
|
||||||
|
arguments: Optional[List[Argument]] = None
|
||||||
|
|
||||||
|
|
||||||
class ExecutionStep(BaseObject):
|
class ExecutionStep(BaseObject):
|
||||||
|
@ -60,12 +61,15 @@ class Execution(BaseObject):
|
||||||
flow: Flow
|
flow: Flow
|
||||||
steps: List[ExecutionStep]
|
steps: List[ExecutionStep]
|
||||||
workspace: Workspace = None # type: ignore
|
workspace: Workspace = None # type: ignore
|
||||||
|
arguments: Optional[List[Argument]] = None
|
||||||
success: bool = False
|
success: bool = False
|
||||||
|
|
||||||
@pydantic.validator("workspace", pre=True, always=True)
|
@pydantic.validator("workspace", pre=True, always=True)
|
||||||
def workspace_default(cls, v): # pylint: disable=no-self-argument
|
def workspace_default(cls, v): # pylint: disable=no-self-argument
|
||||||
return v or Workspace()
|
return v or Workspace()
|
||||||
|
|
||||||
|
# TODO: @pydantic.validator("arguments")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def output(self):
|
def output(self):
|
||||||
for step in self.steps:
|
for step in self.steps:
|
||||||
|
|
|
@ -23,5 +23,5 @@ def test_registry_get_action_ok():
|
||||||
def test_registry_namespace_conflict_ok():
|
def test_registry_namespace_conflict_ok():
|
||||||
|
|
||||||
ActionRegistry.register_action(StubSuccessAction)
|
ActionRegistry.register_action(StubSuccessAction)
|
||||||
with pytest.raises(ActionRegistry.ActionNamespaceConflict):
|
with pytest.raises(ActionRegistry.ActionIDConflict):
|
||||||
ActionRegistry.register_action(StubSuccessAction)
|
ActionRegistry.register_action(StubSuccessAction)
|
||||||
|
|
Reference in New Issue