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

Commit

Permalink
#66: Working example of lifecycle and inheriting
Browse files Browse the repository at this point in the history
  • Loading branch information
blackandred committed Jul 6, 2021
1 parent 57b8c18 commit 7cefae6
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 24 deletions.
Empty file added src/core/rkd/core/py.typed
Empty file.
78 changes: 60 additions & 18 deletions src/core/rkd/core/standardlib/docker.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,56 @@
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
user: str
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:
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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)
Expand Down
65 changes: 60 additions & 5 deletions src/php/rkd/php/script.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
]
4 changes: 3 additions & 1 deletion src/php/tests/samples/.rkd/makefile.py
Original file line number Diff line number Diff line change
@@ -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')
]

0 comments on commit 7cefae6

Please sign in to comment.