diff --git a/localstack/utils/bootstrap.py b/localstack/utils/bootstrap.py index 45026c475a1a5..a2c95f3af9097 100644 --- a/localstack/utils/bootstrap.py +++ b/localstack/utils/bootstrap.py @@ -1,3 +1,4 @@ +import copy import functools import logging import os @@ -7,7 +8,7 @@ import threading import time from functools import wraps -from typing import Dict, Iterable, List, Optional, Set +from typing import Dict, Iterable, Optional, Set from localstack import config, constants from localstack.config import get_edge_port_http, is_env_true @@ -15,9 +16,9 @@ from localstack.runtime import hooks from localstack.utils.container_networking import get_main_container_name from localstack.utils.container_utils.container_client import ( + ContainerConfiguration, ContainerException, PortMappings, - SimpleVolumeBind, VolumeBind, VolumeMappings, ) @@ -365,82 +366,47 @@ def extract_port_flags(user_flags, port_mappings: PortMappings): return user_flags -# TODO merge with docker_utils.py:ContainerConfiguration class LocalstackContainer: - name: str - image_name: str - volumes: VolumeMappings - ports: PortMappings - entrypoint: str - additional_flags: List[str] - command: List[str] - - privileged: bool = True - remove: bool = True - interactive: bool = False - tty: bool = False - detach: bool = False - inherit_env: bool = True - - logfile: Optional[str] = None - stdin: Optional[str] = None - user: Optional[str] = None - cap_add: Optional[List[str]] = None - network: Optional[str] = None - dns: Optional[str] = None - workdir: Optional[str] = None + config: ContainerConfiguration def __init__(self, name: str = None): - self.name = name or config.MAIN_CONTAINER_NAME - self.entrypoint = os.environ.get("ENTRYPOINT", "") - self.command = shlex.split(os.environ.get("CMD", "")) - self.image_name = get_docker_image_to_start() - self.ports = PortMappings(bind_host=config.EDGE_BIND_HOST) - self.volumes = VolumeMappings() - self.env_vars = {} - self.additional_flags = [] - - self.logfile = os.path.join(config.dirs.tmp, f"{self.name}_container.log") - - def _get_mount_volumes(self) -> List[SimpleVolumeBind]: - # FIXME: VolumeMappings should be supported by the docker client - mount_volumes = [] - for volume in self.volumes: - if isinstance(volume, tuple): - mount_volumes.append(volume) - elif isinstance(volume, VolumeBind): - mount_volumes.append((volume.host_dir, volume.container_dir)) - else: - raise NotImplementedError("no support for volume type %s" % type(volume)) - - return mount_volumes + self.config = self._get_default_configuration(name) + self.logfile = os.path.join(config.dirs.tmp, f"{self.config.name}_container.log") + + self.additional_flags = [] # TODO: see comment in run() + + def _get_default_configuration(self, name: str = None) -> ContainerConfiguration: + """Returns a ContainerConfiguration populated with default values or values gathered from the + environment for starting the LocalStack container.""" + return ContainerConfiguration( + image_name=get_docker_image_to_start(), + name=name or config.MAIN_CONTAINER_NAME, + volumes=VolumeMappings(), + remove=True, + # FIXME: update with https://github.com/localstack/localstack/pull/7991 + ports=PortMappings(bind_host=config.EDGE_BIND_HOST), + entrypoint=os.environ.get("ENTRYPOINT"), + command=shlex.split(os.environ.get("CMD", "")) or None, + env_vars={}, + ) def run(self): if isinstance(DOCKER_CLIENT, CmdDockerClient): DOCKER_CLIENT.default_run_outfile = self.logfile + # FIXME: this is pretty awkward, but additional_flags in the LocalstackContainer API was always a + # list of ["-e FOO=BAR", ...], whereas in the DockerClient it is expected to be a string. so we + # need to re-assemble it here. the better way would be to not use additional_flags here all + # together. it is still used in ext in `configure_pro_container` which could be refactored to use + # the additional port bindings. + cfg = copy.deepcopy(self.config) + if not cfg.additional_flags: + cfg.additional_flags = "" + if self.additional_flags: + cfg.additional_flags += " " + " ".join(self.additional_flags) + try: - return DOCKER_CLIENT.run_container( - image_name=self.image_name, - stdin=self.stdin, - name=self.name, - entrypoint=self.entrypoint or None, - remove=self.remove, - interactive=self.interactive, - tty=self.tty, - detach=self.detach, - command=self.command or None, - mount_volumes=self._get_mount_volumes(), - ports=self.ports, - env_vars=self.env_vars, - user=self.user, - cap_add=self.cap_add, - network=self.network, - dns=self.dns, - additional_flags=" ".join(self.additional_flags), - workdir=self.workdir, - privileged=self.privileged, - ) + return DOCKER_CLIENT.run_container_from_config(cfg) except ContainerException as e: if LOG.isEnabledFor(logging.DEBUG): LOG.exception("Error while starting LocalStack container") @@ -454,6 +420,33 @@ def truncate_log(self): with open(self.logfile, "wb") as fd: fd.write(b"") + # these properties are there to not break code that configures the container by using these values + # that code should ideally be refactored soon-ish to use the config instead. + + @property + def env_vars(self) -> Dict[str, str]: + return self.config.env_vars + + @property + def entrypoint(self) -> Optional[str]: + return self.config.entrypoint + + @entrypoint.setter + def entrypoint(self, value: str): + self.config.entrypoint = value + + @property + def name(self) -> str: + return self.config.name + + @property + def volumes(self) -> VolumeMappings: + return self.config.volumes + + @property + def ports(self) -> PortMappings: + return self.config.ports + class LocalstackContainerServer(Server): container: LocalstackContainer @@ -638,7 +631,7 @@ def start_infra_in_docker_detached(console): console.log("configuring container") container = LocalstackContainer() configure_container(container) - container.detach = True + container.config.detach = True container.truncate_log() # start the Localstack container as a Server diff --git a/localstack/utils/container_utils/container_client.py b/localstack/utils/container_utils/container_client.py index c4d83ca952abc..20cebac405e6a 100644 --- a/localstack/utils/container_utils/container_client.py +++ b/localstack/utils/container_utils/container_client.py @@ -18,7 +18,7 @@ if sys.version_info >= (3, 8): from typing import Literal, Protocol, get_args else: - from typing_extensions import Protocol, get_args, Literal + from typing_extensions import Literal, Protocol, get_args from localstack import config from localstack.utils.collections import HashableList, ensure_list @@ -409,7 +409,7 @@ class ContainerConfiguration: volumes: Optional[VolumeMappings] = None ports: Optional[PortMappings] = None entrypoint: Optional[str] = None - additional_flags: Optional[List[str]] = None + additional_flags: Optional[str] = None command: Optional[List[str]] = None env_vars: Dict[str, str] = dataclasses.field(default_factory=dict) @@ -836,7 +836,7 @@ def run_container( tty: bool = False, detach: bool = False, command: Optional[Union[List[str], str]] = None, - mount_volumes: Optional[List[SimpleVolumeBind]] = None, + mount_volumes: Optional[Union[VolumeMappings, List[SimpleVolumeBind]]] = None, ports: Optional[PortMappings] = None, env_vars: Optional[Dict[str, str]] = None, user: Optional[str] = None, @@ -856,6 +856,37 @@ def run_container( :return: A tuple (stdout, stderr) """ + def run_container_from_config( + self, container_config: ContainerConfiguration + ) -> Tuple[bytes, bytes]: + """Like ``run_container`` but uses the parameters from the configuration.""" + + return self.run_container( + image_name=container_config.image_name, + stdin=container_config.stdin, + name=container_config.name, + entrypoint=container_config.entrypoint, + remove=container_config.remove, + interactive=container_config.interactive, + tty=container_config.tty, + detach=container_config.detach, + command=container_config.command, + mount_volumes=container_config.volumes, + ports=container_config.ports, + env_vars=container_config.env_vars, + user=container_config.user, + cap_add=container_config.cap_add, + cap_drop=container_config.cap_drop, + security_opt=container_config.security_opt, + network=container_config.network, + dns=container_config.dns, + additional_flags=container_config.additional_flags, + workdir=container_config.workdir, + platform=container_config.platform, + privileged=container_config.privileged, + ulimits=container_config.ulimits, + ) + @abstractmethod def exec_in_container( self,