diff --git a/src/core/rkd/core/py.typed b/src/core/rkd/core/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/src/core/rkd/core/standardlib/docker.py b/src/core/rkd/core/standardlib/docker.py index d3a90e11..7868fd39 100644 --- a/src/core/rkd/core/standardlib/docker.py +++ b/src/core/rkd/core/standardlib/docker.py @@ -1,17 +1,32 @@ +from abc import ABC + import docker -from typing import List, Dict +from typing import List, Dict, Optional from docker.models.containers import Container from docker.types import Mount from rkd.core.api.contract import ExecutionContext, AbstractExtendableTask from rkd.core.api.lifecycle import ConfigurationLifecycleEventAware -class RunInContainerBaseTask(AbstractExtendableTask, ConfigurationLifecycleEventAware): +class RunInContainerBaseTask(AbstractExtendableTask, ConfigurationLifecycleEventAware, ABC): """ RunInContainerBaseTask ---------------------- Allows to work inside of a temporary docker container. + + Configuration: + mount(): Mount directories/files as volumes + add_file_to_copy(): Copy given files to container before container starts + user: Container username, defaults to "root" + shell: Shell binary path, defaults to "/bin/sh" + docker_image: Full docker image name with registry (optional), group, image name and tag + entrypoint: Entrypoint + command: Command to execute on entrypoint + + Runtime: + copy_to_container(): Copy files/directory to container immediately + in_container(): Execute inside container """ docker_image: str @@ -19,18 +34,23 @@ class RunInContainerBaseTask(AbstractExtendableTask, ConfigurationLifecycleEvent shell: str container: Container to_copy: Dict[str, str] - mountpoints: List[Mount] + mounts: List[Mount] + entrypoint: Optional[str] + command: Optional[str] def __init__(self): self.user = 'root' self.shell = '/bin/sh' self.docker_image = 'alpine:3.13' self.to_copy = {} - self.mountpoints = [] + self.mounts = [] + self.entrypoint = None + self.command = None def get_configuration_attributes(self) -> List[str]: return [ - 'docker_image', 'mount', 'add_file_to_copy', 'user', 'shell' + 'docker_image', 'mount', + 'add_file_to_copy', 'user', 'shell' ] def get_name(self) -> str: @@ -40,7 +60,7 @@ def get_group_name(self) -> str: return ':docker' def execute(self, context: ExecutionContext) -> bool: - self._run_container() + self._run_container(context) try: result = self.inner_execute(context) @@ -51,17 +71,30 @@ def execute(self, context: ExecutionContext) -> bool: return result def mount(self, local: str, remote: str, mount_type: str = 'bind', read_only: bool = False) -> None: - self.mountpoints.append(Mount(target=remote, source=local, type=mount_type, read_only=read_only)) + self.mounts.append(Mount(target=remote, source=local, type=mount_type, read_only=read_only)) def add_file_to_copy(self, local: str, remote: str) -> None: self.to_copy[remote] = local - def in_container(self, cmd: str) -> None: + def in_container(self, cmd: str, workdir: Optional[str] = None, user: Optional[str] = None) -> None: + additional_args = '' + + if workdir: + additional_args += f' -w {workdir} ' + + if user is None: + user = self.user + + if user: + additional_args += f' --user {user} ' + self.io().info_msg(f' >> {cmd}') - self.sh('docker exec {id} {shell} -c "{cmd}"'.format( + self.sh('docker exec {additional_args} -it {id} {shell} -c "{cmd}"'.format( id=self.container.id, shell=self.shell, - cmd=cmd.replace('"', '\"') + cmd=cmd.replace('"', '\"'), + capture=False, + additional_args=additional_args )) def copy_to_container(self, local: str, remote: str) -> None: @@ -79,15 +112,24 @@ def copy_to_container(self, local: str, remote: str) -> None: local=local, remote=remote, container_id=self.container.id )) - def _run_container(self): + def _run_container(self, context: ExecutionContext): client = docker.from_env() - self.container = client.containers.create( - image=self.docker_image, - command='99999999', - entrypoint='sleep', - user=self.user, - mounts=self.mountpoints - ) + env = {} + + for env_name, definition in self.get_declared_envs().items(): + env[env_name] = context.get_env(env_name) + + container_kwargs = { + 'image': self.docker_image, + 'command': self.command, + 'entrypoint': self.entrypoint, + 'user': self.user, + 'mounts': self.mounts, + 'environment': env + } + + self.io().debug(f'Running docker image with args: {container_kwargs}') + self.container = client.containers.create(**container_kwargs) for remote, local in self.to_copy.items(): self.copy_to_container(local=local, remote=remote) diff --git a/src/php/rkd/php/script.py b/src/php/rkd/php/script.py index 9066e0ed..99443d5f 100644 --- a/src/php/rkd/php/script.py +++ b/src/php/rkd/php/script.py @@ -1,6 +1,10 @@ import os +import subprocess +import tempfile from argparse import ArgumentParser -from rkd.core.api.contract import ExecutionContext +from typing import Dict, Union, Optional + +from rkd.core.api.contract import ExecutionContext, ArgumentEnv from rkd.core.api.syntax import TaskDeclaration from rkd.core.execution.lifecycle import ConfigurationLifecycleEvent from rkd.core.standardlib.docker import RunInContainerBaseTask @@ -9,29 +13,80 @@ class PhpScriptTask(RunInContainerBaseTask): """ Execute a PHP code (using a docker container) + Can be extended - this is a base task. + + Inherits settings from `RunInContainerBaseTask`. + + Configuration: + script: Path to script to load instead of stdin (could be a relative path) + """ + script: Optional[str] + + def __init__(self): + super().__init__() + self.user = 'www-data' + self.entrypoint = 'sleep' + self.command = '9999999' + self.script = None + def get_name(self) -> str: return ':php' def get_group_name(self) -> str: return '' + def get_declared_envs(self) -> Dict[str, Union[str, ArgumentEnv]]: + return { + 'PHP': ArgumentEnv('PHP', '--php', '8.0-alpine'), + 'IMAGE': ArgumentEnv('IMAGE', '--image', 'php') + } + def configure(self, event: ConfigurationLifecycleEvent) -> None: - self.docker_image = 'php:{version}'.format(version=event.ctx.get_arg('--php')) + super().configure(event) + + self.docker_image = '{image}:{version}'.format( + image=event.ctx.get_arg_or_env('--image'), + version=event.ctx.get_arg_or_env('--php') + ) self.mount(local=os.getcwd(), remote=os.getcwd()) def inner_execute(self, context: ExecutionContext) -> bool: - self.in_container('ls -la /var') + """ + Execute a code when the container is up and running + :param context: + :return: + """ + + try: + # takes a RKD task input as input file, stdin is interactive + if not self.script and context.get_input(): + with tempfile.NamedTemporaryFile() as tmp_file: + tmp_file.write(context.get_input().read().encode('utf-8')) + tmp_file.flush() + + self.copy_to_container(local=tmp_file.name, remote='/tmp/script.php') + self.in_container('chown www-data:www-data /tmp/script.php', user='root') + + self.in_container('php /tmp/script.php') + return True + + # takes stdin as input + self.in_container(f'php {self.script}') + + except subprocess.CalledProcessError: + self.io().error('PHP process exited with non-zero exit code') + return False return True def configure_argparse(self, parser: ArgumentParser): parser.add_argument('--php', help='PHP version ("php" docker image tag)', default='8.0-alpine') + parser.add_argument('--image', help='Docker image name', default='php') def imports() -> list: return [ - TaskDeclaration(PhpScriptTask(), internal=True), - TaskDeclaration(PhpScriptTask(), internal=True, name=':php2') + TaskDeclaration(PhpScriptTask(), internal=True) ] diff --git a/src/php/tests/samples/.rkd/makefile.py b/src/php/tests/samples/.rkd/makefile.py index a1fc982b..660d794d 100644 --- a/src/php/tests/samples/.rkd/makefile.py +++ b/src/php/tests/samples/.rkd/makefile.py @@ -1,7 +1,9 @@ from rkd.php import ComposerIntegrationTask from rkd.core.api.syntax import TaskDeclaration +from rkd.php.script import PhpScriptTask IMPORTS = [ - TaskDeclaration(ComposerIntegrationTask(), name=':php') + TaskDeclaration(ComposerIntegrationTask(), name=':composer'), + TaskDeclaration(PhpScriptTask(), name=':php') ]