From 11d22a40d45bd79bc04355458a09b3df5e78fe35 Mon Sep 17 00:00:00 2001 From: huji <669898595@qq.com> Date: Fri, 29 Mar 2024 15:26:28 +0800 Subject: [PATCH] support config type 'path' --- src/linktools/_config.py | 40 +++++++++++++------ src/linktools/_environ.py | 14 +++---- .../assets/containers/100-nginx/compose.yml | 14 ++----- .../containers/110-portainer/compose.yml | 15 +++---- .../assets/containers/120-flare/compose.yml | 30 +++++++------- .../assets/containers/120-flare/container.py | 3 +- src/linktools/container/container.py | 38 +++++++++++++----- src/linktools/container/manager.py | 18 ++++----- 8 files changed, 94 insertions(+), 78 deletions(-) diff --git a/src/linktools/_config.py b/src/linktools/_config.py index 73a1c3fe..0ab0ee62 100644 --- a/src/linktools/_config.py +++ b/src/linktools/_config.py @@ -45,10 +45,16 @@ from .rich import prompt, confirm, choose if TYPE_CHECKING: + from typing import Literal from ._environ import BaseEnviron T = TypeVar("T") EnvironType = TypeVar("EnvironType", bound=BaseEnviron) + ConfigType = Literal["path"] + + +def _is_type(obj: Any) -> bool: + return isinstance(obj, type) def _cast_bool(obj: Any) -> bool: @@ -72,11 +78,17 @@ def _cast_str(obj: Any) -> str: return str(obj) -_CONFIG_ENV = "ENV" +def _cast_path(obj: Any) -> str: + if isinstance(obj, str): + return os.path.abspath(os.path.expanduser(obj)) + raise TypeError(f"{type(obj)} cannot be converted to path") + -_CONFIG_TYPES: "Dict[Type[T], Callable[[Any], T]]" = { +_CONFIG_ENV = "ENV" +_CONFIG_TYPES: "Dict[Union[Type[T], ConfigType], Callable[[Any], T]]" = { bool: _cast_bool, str: _cast_str, + "path": _cast_path, } @@ -93,18 +105,20 @@ def optionxform(self, optionstr): class ConfigProperty(abc.ABC): __lock__ = threading.RLock() - def __init__(self, type: Type = None, cached: Union[bool, str] = False): + def __init__(self, type: "Union[Type[T], ConfigType]" = None, cached: Union[bool, str] = False): self._data: Union[str, object] = __missing__ self._type = type self._cached = cached - def load(self, config: "Config", key: str, type: Type = None) -> Any: + def load(self, config: "Config", key: str, type: "Union[Type[T], ConfigType]" = None) -> Any: if self._data is not __missing__: return self._data + with self.__lock__: if self._data is not __missing__: return self._data type = type or self._type + if self._cached: # load cache from config file config_parser = ConfigParser() @@ -121,7 +135,7 @@ def load(self, config: "Config", key: str, type: Type = None) -> Any: result = self._load(config, key, config_cache) if isinstance(result, ConfigProperty): result = result.load(config, key, type=type) - elif type and not isinstance(result, type): + elif not _is_type(type) or not isinstance(result, type): result = config.cast(result, type) # update cache to config file @@ -134,13 +148,15 @@ def load(self, config: "Config", key: str, type: Type = None) -> Any: config_parser.write(fd) self._data = result + else: result = self._load(config, key, __missing__) if isinstance(result, ConfigProperty): result = result.load(config, key, type=type) - elif type and not isinstance(result, type): + elif not _is_type(type) or not isinstance(result, type): result = config.cast(result, type) self._data = result + return self._data @abc.abstractmethod @@ -258,11 +274,11 @@ def load_from_env(self): except Exception as e: self._environ.logger.warning(f"Load config from {self.path} failed: {e}") - def cast(self, obj: Optional[str], type: "Type[T]", default: Any = __missing__) -> "T": + def cast(self, obj: Any, type: "Union[Type[T], ConfigType]", default: Any = __missing__) -> "T": """ 类型转换 """ - if type is not None and type is not __missing__: + if type not in (None, __missing__): cast = _CONFIG_TYPES.get(type, type) try: return cast(obj) @@ -272,7 +288,7 @@ def cast(self, obj: Optional[str], type: "Type[T]", default: Any = __missing__) raise e return obj - def get(self, key: str, type: "Type[T]" = None, default: Any = __missing__) -> "T": + def get(self, key: str, type: "Union[Type[T], ConfigType]" = None, default: Any = __missing__) -> "T": """ 获取指定配置,优先会从环境变量中获取 """ @@ -394,7 +410,7 @@ def __init__( prompt: str = None, password: bool = False, choices: Optional[List[str]] = None, - type: Type[Union[str, int, float]] = str, + type: "Union[Type[Union[str, int, float]], ConfigType]" = str, default: Any = __missing__, cached: Union[bool, str] = False, always_ask: bool = False, @@ -437,7 +453,7 @@ def _load(self, config: "Config", key: str, cache: Any): return prompt( self.prompt or f"Please input {key}", - type=self.type, + type=self.type if not isinstance(self.type, str) else str, password=self.password, choices=self.choices, default=default, @@ -489,7 +505,7 @@ class Alias(ConfigProperty): def __init__( self, *keys: str, - type: Type = str, + type: "Union[Type[T], ConfigType]" = str, default: Any = __missing__, cached: Union[bool, str] = False ): diff --git a/src/linktools/_environ.py b/src/linktools/_environ.py index 0a425c03..6cf71977 100644 --- a/src/linktools/_environ.py +++ b/src/linktools/_environ.py @@ -27,7 +27,6 @@ /_==__==========__==_ooo__ooo=_/' /___________," """ import abc -import getpass import json import logging import os @@ -284,13 +283,14 @@ def _create_tools(self) -> "Tools": index = 0 dir_names = os.environ["PATH"].split(os.pathsep) for tool in tools: + if not tool.executable: + continue # dirname(executable[0]) -> environ["PATH"] - if tool.executable: - dir_name = tool.dirname - if dir_name and dir_name not in dir_names: - # insert to head - dir_names.insert(index, tool.dirname) - index += 1 + dir_name = tool.dirname + if dir_name and dir_name not in dir_names: + # insert to head + dir_names.insert(index, tool.dirname) + index += 1 # add all paths to environment variables os.environ["PATH"] = os.pathsep.join(dir_names) diff --git a/src/linktools/assets/containers/100-nginx/compose.yml b/src/linktools/assets/containers/100-nginx/compose.yml index 811bf68e..9c1caa80 100644 --- a/src/linktools/assets/containers/100-nginx/compose.yml +++ b/src/linktools/assets/containers/100-nginx/compose.yml @@ -1,20 +1,12 @@ services: nginx: - container_name: nginx - build: - context: "{{ container.get_path() }}" - dockerfile: "{{ container.get_docker_file_path() }}" - restart: unless-stopped volumes: - - "{{ container.get_app_path() }}/conf.d:/etc/nginx/conf.d" + - '{{ container.get_app_path("conf.d") }}:/etc/nginx/conf.d' ports: - - "{{ HTTP_PORT }}:{{ HTTP_PORT }}" - - "{{ HTTPS_PORT }}:{{ HTTPS_PORT }}" + - '{{ HTTP_PORT }}:{{ HTTP_PORT }}' + - '{{ HTTPS_PORT }}:{{ HTTPS_PORT }}' networks: - nginx - logging: - options: - max-size: 1m networks: nginx: diff --git a/src/linktools/assets/containers/110-portainer/compose.yml b/src/linktools/assets/containers/110-portainer/compose.yml index d561a2ca..4884e179 100644 --- a/src/linktools/assets/containers/110-portainer/compose.yml +++ b/src/linktools/assets/containers/110-portainer/compose.yml @@ -1,20 +1,15 @@ services: portainer: - container_name: portainer image: portainer/portainer-ce - restart: unless-stopped - {% if int(PORTAINER_EXPOSE_PORT, 0) > 0 %} +# {% if int(PORTAINER_EXPOSE_PORT, 0) > 0 %} ports: - - "{{ PORTAINER_EXPOSE_PORT }}:9000" - {% endif %} + - '{{ PORTAINER_EXPOSE_PORT }}:9000' +# {% endif %} volumes: - - "{{ manager.container_host }}:/var/run/docker.sock" - - "{{ container.get_app_path() }}/data:/data" + - '{{ manager.container_host }}:/var/run/docker.sock' + - '{{ container.get_app_path("data") }}:/data' networks: - nginx - logging: - options: - max-size: 1m networks: nginx: diff --git a/src/linktools/assets/containers/120-flare/compose.yml b/src/linktools/assets/containers/120-flare/compose.yml index 1b9f24e7..9dd66ea8 100644 --- a/src/linktools/assets/containers/120-flare/compose.yml +++ b/src/linktools/assets/containers/120-flare/compose.yml @@ -1,28 +1,26 @@ services: flare: - container_name: flare image: soulteary/flare - user: "{{ DOCKER_PUID }}:{{ DOCKER_PGID }}" - restart: unless-stopped - {% if int(FLARE_EXPOSE_PORT, 0) > 0 %} + user: '{{ DOCKER_PUID }}:{{ DOCKER_PGID }}' +# {% if int(FLARE_EXPOSE_PORT, 0) > 0 %} ports: - - "{{ FLARE_EXPOSE_PORT }}:5005" - {% endif %} - {% if bool(FLARE_DISABLE_LOGIN) %} + - '{{ FLARE_EXPOSE_PORT }}:5005' +# {% endif %} +# {% if bool(FLARE_DISABLE_LOGIN) %} command: flare --disable_login=1 --visibility=private environment: - - FLARE_DISABLE_LOGIN=1 - - FLARE_GUIDE=1 - {% else %} + - 'FLARE_DISABLE_LOGIN=1' + - 'FLARE_GUIDE=1' +# {% else %} command: flare --disable_login=0 --visibility=private environment: - - FLARE_DISABLE_LOGIN=0 - - FLARE_USER={{ FLARE_USER }} - - FLARE_PASS={{ FLARE_PASSWORD }} - - FLARE_GUIDE=1 - {% endif %} + - 'FLARE_DISABLE_LOGIN=0' + - 'FLARE_USER={{ FLARE_USER }}' + - 'FLARE_PASS={{ FLARE_PASSWORD }}' + - 'FLARE_GUIDE=1' +# {% endif %} volumes: - - "{{ container.get_app_path() }}/app:/app" + - '{{ container.get_app_path("app") }}:/app' networks: - nginx diff --git a/src/linktools/assets/containers/120-flare/container.py b/src/linktools/assets/containers/120-flare/container.py index 0360062a..0f4a985d 100644 --- a/src/linktools/assets/containers/120-flare/container.py +++ b/src/linktools/assets/containers/120-flare/container.py @@ -58,8 +58,7 @@ def exposes(self) -> [ExposeLink]: return [ self.expose_other("在线工具集合", "tools", "", "https://tool.lu/"), self.expose_other("在线正则表达式", "regex", "", "https://regex101.com/"), - self.expose_other("正则表达式手册", "regex", "", - "https://tool.oschina.net/uploads/apidocs/jquery/regexp.html"), + self.expose_other("正则表达式手册", "regex", "", "https://tool.oschina.net/uploads/apidocs/jquery/regexp.html"), self.expose_other("在线json解析", "codeJson", "", "https://www.json.cn/"), self.expose_other("DNS查询", "dns", "", "https://tool.chinaz.com/dns/"), self.expose_other("图标下载", "progressDownload", "", "https://materialdesignicons.com/"), diff --git a/src/linktools/container/container.py b/src/linktools/container/container.py index a1f66812..44e7d67f 100644 --- a/src/linktools/container/container.py +++ b/src/linktools/container/container.py @@ -183,13 +183,29 @@ def exposes(self) -> List[ExposeLink]: def docker_compose(self) -> Optional[Dict[str, Any]]: for name in self.manager.docker_compose_names: path = self.get_path(name) - if os.path.exists(path): - data = self.render_template(path) - data = yaml.safe_load(data) - if "services" in data and isinstance(data["services"], dict): - for name, service in data["services"].items(): - if isinstance(service, dict): - service.setdefault("container_name", name) + if not os.path.exists(path): + continue + data = self.render_template(path) + data = yaml.safe_load(data) + if "services" in data and isinstance(data["services"], dict): + for name, service in data["services"].items(): + if not isinstance(service, dict): + continue + service.setdefault("container_name", name) + service.setdefault("restart", "unless-stopped") + service.setdefault("logging", { + "driver": "json-file", + "options": { + "max-size": "10m", + } + }) + if "image" not in service and "build" not in service: + path = self.get_docker_file_path() + if path: + service["build"] = { + "context": self.get_path(), + "dockerfile": path + } return data return None @@ -399,14 +415,16 @@ def render_template(self, source: str, destination: str = None, **kwargs: Any): context.update(kwargs) context.setdefault("DEBUG", self.manager.debug) - context.setdefault("manager", self.manager) - context.setdefault("config", self.manager.config) - context.setdefault("container", self) + context.setdefault("bool", lambda obj, default=False: self.manager.config.cast(obj, type=bool, default=default)) context.setdefault("str", lambda obj, default="": self.manager.config.cast(obj, type=str, default=default)) context.setdefault("int", lambda obj, default=0: self.manager.config.cast(obj, type=int, default=default)) context.setdefault("float", lambda obj, default=0.0: self.manager.config.cast(obj, type=float, default=default)) + context.setdefault("manager", self.manager) + context.setdefault("config", self.manager.config) + context.setdefault("container", self) + template = Template(utils.read_file(source, text=True)) result = template.render(context) if destination: diff --git a/src/linktools/container/manager.py b/src/linktools/container/manager.py index f37714cb..0b2f1bf4 100644 --- a/src/linktools/container/manager.py +++ b/src/linktools/container/manager.py @@ -78,7 +78,7 @@ def __init__(self, environ: "BaseEnviron", name: str = "aio"): # all_in_one ) @property - def debug(self): + def debug(self) -> bool: return os.environ.get("DEBUG", self.environ.debug) @property @@ -90,7 +90,7 @@ def machine(self) -> str: return self.environ.machine @cached_property - def container_type(self): + def container_type(self) -> str: choices = ["docker", "docker-rootless", "podman"] \ if self.system in ("darwin", "linux") and os.getuid() != 0 \ else ["docker", "podman"] @@ -101,7 +101,7 @@ def container_type(self): ) @cached_property - def container_host(self): + def container_host(self) -> str: default = "/var/run/docker.sock" host = self.environ.get_config( "DOCKER_HOST", @@ -114,7 +114,7 @@ def container_host(self): return default @cached_property - def host(self): + def host(self) -> str: return self.environ.get_config( "HOST", type=str, @@ -125,7 +125,7 @@ def host(self): def app_path(self): return self.config.get( "DOCKER_APP_PATH", - type=str, + type="path", default=Config.Prompt( default=Config.Lazy( lambda cfg: self.environ.get_data_path("container", "app", create_parent=True) @@ -138,7 +138,7 @@ def app_path(self): def app_data_path(self): return self.config.get( "DOCKER_APP_DATA_PATH", - type=str, + type="path", default=Config.Prompt( default=Config.Lazy( lambda cfg: self.environ.get_data_path("container", "app_data", create_parent=True) @@ -151,7 +151,7 @@ def app_data_path(self): def user_data_path(self): return self.config.get( "DOCKER_USER_DATA_PATH", - type=str, + type="path", default=Config.Prompt( default=Config.Lazy( lambda cfg: self.environ.get_data_path("container", "user_data", create_parent=True) @@ -164,7 +164,7 @@ def user_data_path(self): def download_path(self): return self.config.get( "DOCKER_DOWNLOAD_PATH", - type=str, + type="path", default=Config.Prompt( default=Config.Lazy( lambda cfg: self.environ.get_data_path("container", "download", create_parent=True) @@ -364,8 +364,6 @@ def create_process( privilege: bool = None, **kwargs ) -> utils.Popen: - if "cwd" not in kwargs: - kwargs["cwd"] = self.environ.get_data_path("container", create_parent=True) if privilege: if self.system in ("darwin", "linux") and self.uid != 0: args = ["sudo", *args]