Skip to content

Commit

Permalink
feat(binary): add global and entrypoint options to binaries (#185)
Browse files Browse the repository at this point in the history
  • Loading branch information
Toilal committed Feb 5, 2021
1 parent 4cbcdec commit 61f973c
Show file tree
Hide file tree
Showing 21 changed files with 247 additions and 125 deletions.
11 changes: 11 additions & 0 deletions ddb/binary/binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ def before_run(self, *args) -> None:
Add action to be executed before running the command.
"""

@property
@abstractmethod
def global_(self) -> bool:
"""
Check if binary should be registered globally.
"""

@abstractmethod
def __eq__(self, other) -> bool:
pass
Expand Down Expand Up @@ -71,6 +78,10 @@ def should_run(self, *args) -> bool:
def priority(self) -> int:
return 0

@property
def global_(self) -> bool:
return False

def __eq__(self, other) -> bool:
if not isinstance(other, AbstractBinary):
return False
Expand Down
43 changes: 33 additions & 10 deletions ddb/cache/shelve_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,44 @@ def __init__(self, namespace: str):
path = os.path.join(tempfile.gettempdir(), "ddb", "cache")
os.makedirs(path, exist_ok=True)

filename = os.path.join(path, self._namespace)
if config.clear_cache and os.path.exists(filename):
os.remove(filename)
self.basename = os.path.join(path, self._namespace)
if config.clear_cache:
self._delete_files(self.basename)
try:
self._shelf = shelve.open(filename)
self._shelf = shelve.open(self.basename)
except Exception as open_error: # pylint:disable=broad-except
if os.path.exists(filename):
if self._delete_files(self.basename):
try:
os.remove(filename)
self._shelf = shelve.open(filename)
self._shelf = shelve.open(self.basename)
except Exception as fallback_error: # pylint:disable=broad-except
raise open_error from fallback_error
else:
raise open_error
if config.clear_cache:
self._shelf.clear()

@staticmethod
def _delete_files(basename):
deleted = False
for filename in (basename, f"{basename}.dat", f"{basename}.dir"):
if os.path.exists(filename):
os.remove(filename)
deleted = True
return deleted

def close(self):
self._shelf.close()

def flush(self):
self._shelf.sync()

def get(self, key: str, default=None):
return self._shelf.get(key, default)
try:
return self._shelf.get(key, default)
except AttributeError:
# This can occur when class definition hash change.
self._delete_files(self.basename)
raise

def keys(self):
return self._shelf.keys()
Expand All @@ -65,10 +78,20 @@ def set(self, key: str, data):
self._shelf[key] = data

def pop(self, key: str):
return self._shelf.pop(key)
try:
return self._shelf.pop(key)
except AttributeError:
# This can occur when class definition hash change.
self._delete_files(self.basename)
raise

def clear(self):
self._shelf.clear()
try:
self._shelf.clear()
except AttributeError:
# This can occur when class definition hash change.
self._delete_files(self.basename)
raise

def __contains__(self, key):
return self._shelf.__contains__(key)
2 changes: 2 additions & 0 deletions ddb/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def get_default_config_paths(env_prefix, filenames, extensions) -> ConfigPaths:
project_home_candidate = project_home_candidate_parent
project_home = project_home_candidate

project_home = os.path.abspath(project_home)

home = os.environ.get(env_prefix + '_HOME', os.path.join(str(Path.home()), '.docker-devbox'))
ddb_home = os.environ.get(env_prefix + '_DDB_HOME', os.path.join(home, 'ddb'))

Expand Down
2 changes: 0 additions & 2 deletions ddb/config/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,6 @@ def __getitem__(self, item):
PropertyMigration("docker.compose.file_version",
"jsonnet.docker.compose.version", since="v1.6.0"),

PropertyMigration("docker.path_mapping",
"jsonnet.docker.path_mapping", since="v1.6.0"),
PropertyMigration("docker.disabled_services",
"jsonnet.docker.compose.excluded_services", since="v1.6.0"),

Expand Down
2 changes: 1 addition & 1 deletion ddb/event/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def docker_compose_after_events(self, docker_compose_config: dict):

@event("docker:binary")
def binary(self, name=None, workdir=None, options=None, options_condition=None, condition=None, args=None,
docker_compose_service=None):
exe=False, entrypoint=None, global_=None, docker_compose_service=None):
"""
Ask a binary to be generated from docker compose service
"""
Expand Down
24 changes: 24 additions & 0 deletions ddb/feature/docker/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import os
import pathlib
import re
from typing import Iterable, ClassVar

Expand All @@ -12,6 +13,7 @@
from ..feature import Feature, FeatureConfigurationAutoConfigureError
from ..schema import FeatureSchema
from ...action import Action
from ...config import config


class DockerFeature(Feature):
Expand Down Expand Up @@ -44,6 +46,7 @@ def _configure_defaults(self, feature_config: Dotty):
self._configure_defaults_ip(feature_config)
self._configure_defaults_user_from_name_and_group(feature_config)
self._configure_defaults_user(feature_config)
self._configure_defaults_path_mapping(feature_config)

def _configure_defaults_ip(self, feature_config):
ip_address = feature_config.get('ip')
Expand Down Expand Up @@ -128,3 +131,24 @@ def _configure_defaults_user(feature_config):
except AttributeError:
gid = 1000
feature_config['user.gid'] = gid

@staticmethod
def _configure_defaults_path_mapping(feature_config):
"""
On windows, this generates a default path mapping matching docker-compose behavior when
COMPOSE_CONVERT_WINDOWS_PATHS=1 is enabled.
Drive letter should be lowercased to have the same behavior
https://github.com/docker/compose/blob/f1059d75edf76e8856469108997c15bb46a41777/compose/config/types.py#L123-L132
"""
path_mapping = feature_config.get('path_mapping')
if path_mapping is None:
path_mapping = {}
if config.data.get('core.os') == 'nt':
raw = config.data.get('core.path.project_home')
mapped = re.sub(r"^([a-zA-Z]):", r"/\1", raw)
mapped = pathlib.Path(mapped).as_posix()
mapped = re.sub(r"(\/)(.)(\/.*)", lambda x: x.group(1) + x.group(2).lower() + x.group(3), mapped)
path_mapping[raw] = mapped
feature_config['path_mapping'] = path_mapping
9 changes: 7 additions & 2 deletions ddb/feature/docker/actions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import os
import re
import keyword
from pathlib import PurePosixPath, Path
from typing import Union, Iterable, List, Dict, Set

Expand Down Expand Up @@ -137,6 +138,9 @@ def _build_event_data(self, docker_compose_config, labels, service, service_name
event_id = match.group(2)
property_name = match.group(3)

if property_name and property_name in keyword.kwlist:
property_name = property_name + '_'

args, kwargs = self._parse_value(value,
{"service": service, "config": docker_compose_config},
property_name)
Expand Down Expand Up @@ -238,7 +242,7 @@ def after_events(self, docker_compose_config):
docker_binaries_cache.flush()

def execute(self, name=None, workdir=None, options=None, options_condition=None, condition=None, args=None,
exe=False, docker_compose_service=None):
exe=False, entrypoint=None, global_=None, docker_compose_service=None):
"""
Execute action
"""
Expand All @@ -248,7 +252,8 @@ def execute(self, name=None, workdir=None, options=None, options_condition=None,
raise ValueError("name should be defined")

binary = DockerBinary(name, docker_compose_service=docker_compose_service, workdir=workdir, options=options,
options_condition=options_condition, condition=condition, args=args, exe=exe)
options_condition=options_condition, condition=condition, args=args, exe=exe,
entrypoint=entrypoint, global_=global_)

self.binaries.add(binary)

Expand Down
59 changes: 44 additions & 15 deletions ddb/feature/docker/binaries.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import os
import posixpath
import shlex
from pathlib import Path
from typing import Optional, Iterable

from simpleeval import simple_eval

from pathlib import Path

from ddb.binary.binary import AbstractBinary
from ddb.config import config
from ddb.feature.docker.utils import get_mapped_path
from ddb.utils.docker import DockerUtils
from ddb.utils.process import effective_command


class DockerBinary(AbstractBinary):
class DockerBinary(AbstractBinary): # pylint:disable=too-many-instance-attributes
"""
Binary to run docker compose command.
"""
Expand All @@ -26,7 +26,9 @@ def __init__(self,
options_condition: Optional[str] = None,
condition: Optional[str] = None,
args: Optional[str] = None,
exe: bool = False):
exe: bool = False,
entrypoint: Optional[str] = None,
global_: bool = False):
super().__init__(name)
self.docker_compose_service = docker_compose_service
self.workdir = workdir
Expand All @@ -35,26 +37,46 @@ def __init__(self,
self.condition = condition
self.args = args
self.exe = exe
self.entrypoint = entrypoint
self._global = global_

@staticmethod
def simple_eval_options(*args):
"""
Retrieve the simple_eval options
"""
return dict(functions={},
names={'args': ' '.join(args),
'argv': args,
'config': config,
'cwd': str(Path(config.cwd).as_posix()) if config.cwd else None,
'project_cwd': str(Path(config.project_cwd).as_posix()) if config.project_cwd else None})
names={"args": " ".join(args),
"argv": args,
"config": config,
"cwd": str(Path(config.cwd).as_posix()) if config.cwd else None,
"project_cwd": str(Path(config.project_cwd).as_posix()) if config.project_cwd else None})

def command(self, *args) -> Iterable[str]:
params = ["exec"] if hasattr(self, 'exe') and self.exe else ["run", "--rm"]
cwd = config.cwd if config.cwd else os.getcwd()
real_cwd = os.path.realpath(cwd)
real_project_home = os.path.realpath(config.paths.project_home)

if self.workdir:
relpath = os.path.relpath(config.cwd if config.cwd else os.getcwd(), config.paths.project_home)
container_workdir = posixpath.join(self.workdir, relpath)
params.append("--workdir=%s" % (container_workdir,))
if real_cwd.startswith(real_project_home):
params = ["exec"] if hasattr(self, "exe") and self.exe else ["run", "--rm"]

if self.workdir:
relpath = os.path.relpath(cwd, config.paths.project_home)
container_workdir = posixpath.join(self.workdir, relpath)
params.append(f"--workdir={container_workdir}")
else:
# cwd is outside of project home.
project_relpath = os.path.relpath(config.paths.project_home, config.cwd if config.cwd else os.getcwd())
params = ["-f", os.path.join(project_relpath, "docker-compose.yml")]
params.append("run")
params.append("--rm")
if self.workdir:
mapped_cwd = get_mapped_path(real_cwd)
params.append(f"--volume={mapped_cwd}:{self.workdir}")
params.append(f"--workdir={self.workdir}")

if self.entrypoint:
params.append(f"--entrypoint={self.entrypoint}")

self.add_options_to_params(params, self.options, self.options_condition, *args)

Expand Down Expand Up @@ -90,6 +112,10 @@ def should_run(self, *args) -> bool:
return bool(simple_eval(self.condition, **self.simple_eval_options(*args)))
return super().should_run(*args)

@property
def global_(self) -> bool:
return self._global

def __lt__(self, other):
"""
This is used to order binaries in run feature action.
Expand Down Expand Up @@ -128,6 +154,8 @@ def __eq__(self, other): # pylint:disable=too-many-return-statements
return False
if self.exe != other.exe:
return False
if self.entrypoint != other.entrypoint:
return False

return True

Expand All @@ -138,4 +166,5 @@ def __hash__(self):
self.options,
self.options_condition,
self.args,
self.exe))
self.exe,
self.entrypoint))
1 change: 1 addition & 0 deletions ddb/feature/docker/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ class DockerSchema(FeatureSchema):
ip = fields.String(required=True, default=None) # default is set in feature _configure_defaults
interface = fields.String(required=True, default="docker0")
user = fields.Nested(UserSchema(), default=UserSchema())
path_mapping = fields.Dict(required=False) # default is set in feature _configure_defaults
19 changes: 19 additions & 0 deletions ddb/feature/docker/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from ddb.config import config


def get_mapped_path(path: str):
"""
Get docker mapped path, using `docker.path_mapping` configuration.
:param path:
:return:
"""
path_mapping = config.data.get('docker.path_mapping')
fixed_path = None
fixed_source_path = None
if path_mapping:
for source_path, target_path in path_mapping.items():
if path.startswith(source_path) and \
(not fixed_source_path or len(source_path) > len(fixed_source_path)):
fixed_source_path = source_path
fixed_path = target_path + path[len(source_path):]
return fixed_path if fixed_path else path
23 changes: 0 additions & 23 deletions ddb/feature/jsonnet/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import hashlib
import os
import pathlib
import re
from typing import ClassVar, Iterable

Expand Down Expand Up @@ -45,7 +44,6 @@ def _configure_defaults(self, feature_config: Dotty):
self._configure_defaults_user(feature_config)
self._configure_defaults_user_maps(feature_config)
self._configure_defaults_xdebug(feature_config)
self._configure_defaults_path_mapping(feature_config)
self._configure_defaults_port_prefix(feature_config)
self._configure_defaults_compose_project_name(feature_config)
self._configure_defaults_build_image_tag(feature_config)
Expand Down Expand Up @@ -157,27 +155,6 @@ def _configure_defaults_xdebug(feature_config):
else:
feature_config['docker.xdebug.disabled'] = False

@staticmethod
def _configure_defaults_path_mapping(feature_config):
"""
On windows, this generates a default path mapping matching docker-compose behavior when
COMPOSE_CONVERT_WINDOWS_PATHS=1 is enabled.
Drive letter should be lowercased to have the same behavior
https://github.com/docker/compose/blob/f1059d75edf76e8856469108997c15bb46a41777/compose/config/types.py#L123-L132
"""
path_mapping = feature_config.get('docker.path_mapping')
if path_mapping is None:
path_mapping = {}
if config.data.get('core.os') == 'nt':
raw = config.data.get('core.path.project_home')
mapped = re.sub(r"^([a-zA-Z]):", r"/\1", raw)
mapped = pathlib.Path(mapped).as_posix()
mapped = re.sub(r"(\/)(.)(\/.*)", lambda x: x.group(1) + x.group(2).lower() + x.group(3), mapped)
path_mapping[raw] = mapped
feature_config['docker.path_mapping'] = path_mapping

@staticmethod
def _configure_defaults_port_prefix(feature_config):
port_prefix = feature_config.get('docker.expose.port_prefix')
Expand Down
Loading

0 comments on commit 61f973c

Please sign in to comment.