示例#1
0
class PlaygroundDockerClient(object):
    _envoy_label = "envoy.playground"

    def __init__(self, api):
        self.api = api
        self.docker = aiodocker.Docker()
        self.images = PlaygroundDockerImages(self)
        self.volumes = PlaygroundDockerVolumes(self)
        self.proxies = PlaygroundDockerProxies(self)
        self.services = PlaygroundDockerServices(self)
        self.networks = PlaygroundDockerNetworks(self)
        self.events = PlaygroundDockerEvents(self)
        self._subscribers = []

    @method_decorator(cmd)
    async def clear(self) -> None:
        await self.networks.clear()
        await self.proxies.clear()
        await self.services.clear()

    @method_decorator(cmd(sync=True))
    async def dump_resources(self) -> list:
        proxies = {proxy['name']: proxy for proxy in await self.proxies.list()}
        services = {
            service['name']: service
            for service in await self.services.list()
        }
        networks = {
            network['name']: network
            for network in await self.networks.list()
        }
        return dict(networks=networks, proxies=proxies, services=services)

    async def emit_error(self, message: str):
        for _subscriber in self._subscribers:
            await _subscriber(message)

    async def get_container(self, id: str):
        return await self.docker.containers.get(id)

    async def get_network(self, id: str):
        return await self.docker.networks.get(id)

    def subscribe(self, handler, debug):
        errors = handler.pop("errors", None)
        if errors:
            self._subscribers.append(errors)
        self.events.subscribe(handler, debug)
示例#2
0
class PlaygroundDockerServices(PlaygroundDockerResources):
    _docker_resource = 'containers'
    name = 'service'

    @method_decorator(cmd(attribs=ServiceCreateCommandAttribs))
    async def create(self, command: PlaygroundCommand) -> None:
        logger.debug(f'Creating service: {command.data.name}')
        if not command.data.image:
            # todo: add build logic
            return
        if not await self.connector.images.exists(command.data.image):
            await self.connector.events.publish('image_pull', 'service',
                                                command.data.name,
                                                command.data.image)
            if not await self.connector.images.pull(command.data.image):
                logger.debug(f'Failed pulling image ({command.data.image}) '
                             f'for {command.data.name}')
                return
        _environment = ["%s=%s" % (k, v) for k, v in command.data.env.items()]
        await self._start_container(
            self._get_service_config(
                command.data.service_type, command.data.image,
                command.data.name, _environment, await
                self._get_service_mounts(command.data)), command.data.name)

    async def _get_service_mounts(
            self, data: ServiceCreateCommandAttribs) -> OrderedDict:
        mounts = OrderedDict()
        if data.configuration and data.config_path:
            config = base64.b64encode(
                data.configuration.encode('utf-8')).decode()
            mounts[os.path.dirname(
                data.config_path)] = (await self.connector.volumes.populate(
                    'service', f'{data.service_type}:{data.name}', 'config',
                    {os.path.basename(data.config_path): config}))
        return mounts

    def _get_service_config(self, service_type: str, image: str, name: str,
                            environment: list, mounts: dict) -> dict:
        labels = {
            "envoy.playground.service": name,
            "envoy.playground.service.type": service_type,
        }
        return {
            'Image': image,
            "AttachStdin": False,
            "AttachStdout": False,
            "AttachStderr": False,
            "Tty": False,
            "OpenStdin": False,
            "Env": environment,
            "Labels": labels,
            "HostConfig": {
                "Binds": ['%s:%s' % (v, k) for k, v in mounts.items()]
            }
        }

    async def _mangle_resource(self, resource, _resource):
        _resource['image'] = resource['Image']
        _resource['type'] = 'service'
        _resource["service_type"] = resource["Labels"][
            "envoy.playground.service.type"]
示例#3
0
class PlaygroundDockerNetworks(PlaygroundDockerResources):
    _docker_resource = 'networks'
    name = 'network'

    @method_decorator(cmd(attribs=NetworkAddAttribs))
    async def create(
            self,
            command: PlaygroundCommand) -> None:
        logger.debug(
            f'Creating network: {command.data}')
        try:
            await self._create_network(command)
        except DockerError as e:
            logger.error(
                f'Failed creating network: {command.data} {e}')

    @method_decorator(cmd(attribs=NetworkDeleteAttribs))
    async def delete(
            self,
            command: PlaygroundCommand) -> None:
        logger.debug(
            f'Deleting network: {command.data}')
        try:
            await self._delete_network(command)
        except DockerError as e:
            logger.error(
                f'Failed deleting network: {command.data} {e}')

    @method_decorator(cmd(attribs=NetworkEditAttribs))
    async def edit(
            self,
            command: PlaygroundCommand) -> None:
        logger.debug(
            f'Updating network: {command.data}')
        try:
            await self._edit_network(command)
        except DockerError as e:
            logger.error(
                f'Failed updating network: {command.data} {e}')

    async def _connect(self, network, name, containers):
        for container in containers:
            logger.debug(
                f'Connecting {container["type"]} {container["name"]} '
                f'to network: {name}')
            try:
                await network.connect(self._get_container_config(container))
            except DockerError as e:
                logger.debug(
                    f'Failed connecting {container["type"]} '
                    f'{container["name"]}'
                    f'to network: {name} {e}')
                continue
            if container['type'] == 'proxy':
                try:
                    await self.docker.containers.container(
                        container['id']).kill(signal='SIGHUP')
                except DockerError as e:
                    logger.error(
                        f'Failed restarting proxy on connect: '
                        f'{container["name"]} {e}')

    async def _create_network(
            self,
            command: PlaygroundCommand) -> None:
        logger.debug(
            f'Creating network: {command.data}')
        try:
            network = await self.docker.networks.create(
                dict(name="__playground_%s" % command.data.name,
                     labels={"envoy.playground.network": command.data.name}))
        except DockerError as e:
            logger.error(
                f'Failed creating network: {command.data} {e}')
            return
        await self._connect(
            network,
            command.data.name,
            (container
             for container in await self.connector.proxies.list()
             if container['name'] in command.data.proxies))
        await self._connect(
            network,
            command.data.name,
            (container
             for container in await self.connector.services.list()
             if container['name'] in command.data.services))

    async def _delete_network(
            self,
            command: PlaygroundCommand) -> None:
        logger.debug(
            f'Deleting network: {command.data}')
        try:
            network = await self.docker.networks.get(command.data.id)
            info = await network.show()
        except DockerError as e:
            logger.error(
                f'Failed getting network for delete: {command.data} {e}')
            return
        if "envoy.playground.network" not in info["Labels"]:
            logger.warng(
                f'Received spurious network: {command.data}')
            return
        for container in info["Containers"].keys():
            logger.debug(
                f'Disconnecting ({container}) from network: '
                f'{command.data}')
            try:
                await network.disconnect({"Container": container})
            except DockerError as e:
                logger.error(
                    f'Failed disconnecting container: '
                    f'{command.data} {container} {e}')
        try:
            await network.delete()
        except DockerError as e:
            logger.error(
                f'Failed removing network: '
                f'{command.data} {e}')

    async def _disconnect(self, network, name, containers):
        for container in containers:
            logger.debug(
                f'Disconnecting {container["type"]} {container["name"]} '
                f'from network: {name}')
            try:
                await network.disconnect({"Container": container["id"]})
            except DockerError as e:
                logger.error(
                    f'Failed disconnecting container: '
                    f'{name} {container["type"]} {container["name"]} {e}')

    async def _edit_network(
            self,
            command: PlaygroundCommand) -> None:
        network = await self.docker.networks.get(command.data.id)
        info = await network.show()
        containers = {
            container['Name'].replace(
                'envoy__playground__service__', '').replace(
                    'envoy__playground__proxy__', '')
            for container
            in info["Containers"].values()}
        expected = (
            set(command.data.proxies or [])
            | set(command.data.services or []))
        connect = expected - containers
        disconnect = containers - expected
        _containers = (
            await self.connector.proxies.list()
            + await self.connector.services.list())
        await self._connect(
            network,
            info['Labels']['envoy.playground.network'],
            (container
             for container in _containers
             if container['name'] in connect))
        await self._disconnect(
            network,
            info['Labels']['envoy.playground.network'],
            (container
             for container in _containers
             if container['name'] in disconnect))

    def _get_container_config(self, container):
        return {
            "Container": container["id"],
            "EndpointConfig": {
                "Aliases": [container['name']]}}

    async def _mangle_resource(self, resource, _resource):
        try:
            _actual_network = await self.docker.networks.get(resource["Id"])
            info = await _actual_network.show()
        except DockerError as e:
            logger.error(
                f'Failed getting info for network: {resource} {e}')
        else:
            if info["Containers"]:
                _resource["containers"] = [
                    container[:10]
                    for container
                    in info["Containers"].keys()]
示例#4
0
class PlaygroundDockerProxies(PlaygroundDockerResources):
    _docker_resource = 'containers'
    name = 'proxy'

    @method_decorator(cmd(attribs=ProxyCreateCommandAttribs))
    async def create(self, command: PlaygroundCommand) -> None:
        logger.debug(f'Creating proxy: {command.data.name}')
        use_dev = (not command.data.version
                   or command.data.version.startswith('envoy-dev'))
        image = ('envoyproxy/envoy-dev' if use_dev else 'envoyproxy/envoy')
        tag = (command.data.version.split(':')[1]
               if ':' in command.data.version else 'latest')
        base_image = f'{image}:{tag}'
        envoy_image = (f"envoyproxy/{base_image.split('/')[1].split(':')[0]}"
                       f"-playground:{base_image.split(':')[1]}")
        should_pull = (command.data.pull_latest
                       or not await self.connector.images.exists(envoy_image))
        if should_pull:
            await self.connector.events.publish('image_pull', 'proxy',
                                                command.data.name, base_image)
            if await self.connector.images.pull(base_image):
                await self.connector.events.publish('image_build',
                                                    command.data.name,
                                                    base_image, envoy_image)
                built = await self.connector.images.build(
                    base_image, envoy_image)
            if not built:
                # todo: publish failure ?
                logger.error(f'Failed building proxy image for {base_image}')
                return
        _mappings = [[m['mapping_from'], m['mapping_to']]
                     for m in command.data.port_mappings]

        config = self._get_proxy_config(envoy_image, command.data.name,
                                        command.data.logging, await
                                        self._get_mounts(command.data),
                                        _mappings)
        await self._start_container(config, command.data.name)

    async def _get_mounts(self, data: ProxyCreateCommandAttribs) -> dict:
        return {
            "/etc/envoy":
            await self.connector.volumes.populate(
                'proxy', data.name, 'envoy', {
                    'envoy.yaml':
                    base64.b64encode(
                        data.configuration.encode('utf-8')).decode()
                }),
            "/certs":
            await self.connector.volumes.populate('proxy', data.name, 'certs',
                                                  data.certs),
            '/binary':
            await self.connector.volumes.populate('proxy', data.name, 'binary',
                                                  data.binaries),
            '/logs':
            await self.connector.volumes.create('proxy', data.name, 'logs')
        }

    def _get_port_bindings(self, port_mappings: list) -> OrderedDict:
        # todo: handle udp etc
        port_bindings: OrderedDict = OrderedDict()
        for host, internal in port_mappings:
            port_bindings[f"{internal}/tcp"] = port_bindings.get(
                f"{internal}/tcp", [])
            port_bindings[f"{internal}/tcp"].append({"HostPort": f"{host}"})
        return port_bindings

    def _get_proxy_config(self, image: str, name: str, logging: dict,
                          mounts: dict, port_mappings: list) -> dict:
        environment = ([] if logging.get("default", "info") in ['', "info"]
                       else [f"ENVOY_LOG_LEVEL={logging['default']}"])
        exposed = {
            f"{internal}/tcp": {}
            for external, internal in port_mappings
        }
        return {
            'Image': image,
            'Cmd': ["python", "/hot-restarter.py", "/start_envoy.sh"],
            "AttachStdin": False,
            "AttachStdout": False,
            "AttachStderr": False,
            "Tty": False,
            "OpenStdin": False,
            "Labels": {
                "envoy.playground.proxy": name,
            },
            "Env": environment,
            "ExposedPorts": exposed,
            "HostConfig": {
                "PortBindings": self._get_port_bindings(port_mappings),
                "Binds": ['%s:%s' % (v, k) for k, v in mounts.items()]
            }
        }

    async def _mangle_resource(self, resource, _resource):
        _resource['image'] = self._get_image_name(resource['Image'])
        _resource['port_mappings'] = [{
            'mapping_from': m.get('PublicPort'),
            'mapping_to': m.get('PrivatePort')
        } for m in resource['Ports'] if m.get('PublicPort')]
        if not _resource['port_mappings']:
            del _resource['port_mappings']
示例#5
0
class PlaygroundDockerResources(PlaygroundDockerContext):
    async def clear(self):
        for resource in await self.list():
            await self.delete(dict(id=resource['id']))

    async def get(self, uuid):
        types = {
            f'envoy.playground.{container_type}'
            for container_type in ['service', 'proxy']
        }
        try:
            container = await self.docker.containers.get(uuid)
        except DockerError:
            logger.error(f'Failed getting container {uuid}')
            return
        return (container if types.intersection(
            container['Config']['Labels'].keys()) else None)

    @method_decorator(cmd(attribs=ContainerDeleteAttribs))
    async def delete(self, command: PlaygroundCommand) -> None:
        logger.debug(f'Deleting {self.name}: {command.data.id}')
        container = await self.get(command.data.id)
        if not container:
            logger.warning(
                f'Unable to find container to delete: {command.data.id}')
            return
        await self._delete_container(container)

    async def _delete_container(self, container):
        try:
            await container.stop()
            await container.wait()
            await container.delete(v=True, force=True)
            volumes = [
                v['Name'] for v in container.__dict__['_container']['Mounts']
            ]
            await self.connector.volumes.delete(volumes)
            return True
        except DockerError as e:
            logger.warning(f'Failed deleting {container}: {e}')
            if e.args[0] == 409:
                raise PlaytimeError(e.args[1]['message'])
            return False

    def _get_image_name(self, playground_image):
        envoy_image = playground_image.split('/')[1].split(':')[0].replace(
            '-playground', '')
        tag = playground_image.split(':')[1]
        return f"envoyproxy/{envoy_image}:{tag}"

    async def list(self) -> list:
        if not self._docker_resource or not self.name:
            return []
        return await self._list_resources(
            getattr(self.docker, self._docker_resource), self.name)

    async def _list_resources(self, resources: Union[DockerContainers,
                                                     DockerNetworks],
                              name: str) -> list:
        _resources = []
        label = "%s.%s" % (self.connector._envoy_label, name)
        try:
            docker_resources = await resources.list()
        except DockerError:
            logger.error(f'Failed listing containers: {name}')
            return []
        for resource in docker_resources:
            if label not in resource["Labels"]:
                continue
            _resource = dict(name=resource["Labels"][label],
                             id=resource["Id"][:10],
                             type=name)
            await self._mangle_resource(resource, _resource)
            _resources.append(_resource)
        return _resources

    async def _remove_container(self, container):
        volumes = [v['Name'] for v in container['Mounts']]
        try:
            await self._delete_container(container)
        except PlaytimeError as e:
            logger.error(f'Failed removing container {e}')
            await self.connector.emit_error(e.args[0])
            return
        if volumes:
            await self._remove_volumes(volumes)

    async def _remove_volumes(self, volumes):
        _volumes = await self.docker.volumes.list()
        for volume in _volumes['Volumes']:
            volume_name = volume['Name']
            if volume_name not in volumes:
                continue
            volume_delete = self.docker._query(f"volumes/{volume_name}",
                                               method="DELETE")
            try:
                async with volume_delete:
                    pass
            except DockerError as e:
                logger.error(f'Failed removing volume {volume_name} {e}')

    async def _start_container(self, config, name):
        logger.debug(f'Starting {self.name}: {name}')
        try:
            container = await self.docker.containers.create_or_replace(
                config=config, name=f"envoy__playground__{self.name}__{name}")
            await container.start()
        except DockerError as e:
            logger.error(f'Failed starting container {name} {config} {e}')