diff --git a/.gitignore b/.gitignore index eba624a..6be52ba 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ !src/* !.gitignore !default* -!main.py \ No newline at end of file +!main.py +!requirements.txt \ No newline at end of file diff --git a/defaultenv b/defaultenv index 41c5d57..5996df5 100644 --- a/defaultenv +++ b/defaultenv @@ -2,5 +2,5 @@ DOCKER_INIPATH = docker.ini NGINX_INIPATH = nginx.ini DOCKER_COMPOSE_OUTPATH = docker-compose.yml -INNER_NGINX_OUTPATH = innner-nginx.conf +INNER_NGINX_OUTPATH = inner-nginx.conf OUTER_NGINX_OUTPATH = outer-nginx.conf \ No newline at end of file diff --git a/main.py b/main.py index eec78c2..eab8178 100644 --- a/main.py +++ b/main.py @@ -6,63 +6,111 @@ import os import subprocess from pathlib import Path from dotenv import load_dotenv -from jinja2_render import render_dataclass +from src.jinja2_render import render_dataclass from src.config_reader import nginx_config, docker_config, docker2nginx load_dotenv() def run_command(command): + "run a command and use stdout" try: result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) print(result.stdout.decode()) + return True except subprocess.CalledProcessError as e: print(f"Command failed with error: {e.stderr.decode()}") + return False -def docker_compose_action(): - config_path = Path(os.getenv('DOCKER_CONFIG_PATH', 'docker.ini')) - output_path = Path(os.getenv('DOCKER_COMPOSE_OUTPATH', 'docker-compose.conf')) - +def docker_compose_action(config_path, output_path): + "读取并渲染配置文件" (auto_config, other) = docker_config(config_path) - render_dataclass(auto_config, Path('./src/docker-compose.j2'), output_path, other) - -def inner_nginx_action(): - config_path = Path(os.getenv('DOCKER_CONFIG_PATH', 'docker.ini')) - output_path = Path(os.getenv('INNER_NGINX_OUTPATH', 'inner-nginx.conf')) + render_dataclass(auto_config, Path('./src/docker-compose.j2'), output_path, other=other) +def inner_nginx_action(config_path, output_path): + "读取并渲染配置文件" (auto_config, other) = docker2nginx(config_path) - render_dataclass(auto_config, Path('./src/nginx-conf.j2'), output_path, other) - -def outer_nginx_action(): - config_path = Path(os.getenv('NGINX_CONFIG_PATH', 'docker.ini')) - output_path = Path(os.getenv('OUTER_NGINX_OUTPATH', 'outer-nginx.conf')) + render_dataclass(auto_config, Path('./src/nginx-conf.j2'), output_path, other=other) +def outer_nginx_action(config_path, output_path): + "读取并渲染配置文件" (auto_config, other) = nginx_config(config_path) - render_dataclass(auto_config, Path('./src/nginx-conf.j2'), output_path, other) + render_dataclass(auto_config, Path('./src/nginx-conf.j2'), output_path, other=other) + +def nginx_reload(): + """reload nginx config""" + res = True + if res: + res = run_command("docker exec -it nginx nginx -t") + if res: + res = run_command("docker exec -it nginx nginx -s reload") + +def docker_compose_restart(): + "docker compose restart" + run_command("docker compose restart") + +def docker_ps(): + "list running docker containers by my format" + cmd = "docker ps --format "\ + "\"table {{.Names}}\t{{.Image}}\t{{.Command}}\t{{.CreatedAt}}\t{{.Status}}\"" + run_command(cmd) def main(): - + "main function" parser = argparse.ArgumentParser(description="Automate Docker and Nginx configuration.") + parser.add_argument('-a', '--all', action='store_true', default=True, + help='Perform all actions') parser.add_argument('-c', '--docker-compose', action='store_true', help='Generate Docker Compose') parser.add_argument('-i', '--inner-nginx', action='store_true', help='Generate Inner Nginx (docker network)') parser.add_argument('-o', '--outer-nginx', action='store_true', help='Generate Outer Nginx (host machine)') + parser.add_argument('-r', '--nginx-reload', action='store_true', + help='Reload Nginx configuration') + parser.add_argument('-s', '--docker-compose-restart', action='store_true', + help='Restart Docker Compose services') + parser.add_argument('-p', '--docker-ps', action='store_true', + help='List running Docker containers') + parser.add_argument('--docker-config-path', type=Path, + default=Path(os.getenv('DOCKER_INIPATH', 'docker.ini')), + help='Path to the Docker INI configuration file') + parser.add_argument('--docker-output-path', type=Path, + default=Path(os.getenv('DOCKER_COMPOSE_OUTPATH', 'docker-compose.conf')), + help='Path to the output Docker Compose configuration file') + parser.add_argument('--inner-config-path', type=Path, + default=Path(os.getenv('DOCKER_INIPATH', 'docker.ini')), + help='Path to the Docker INI configuration file for inner Nginx') + parser.add_argument('--inner-output-path', type=Path, + default=Path(os.getenv('INNER_NGINX_OUTPATH', 'inner-nginx.conf')), + help='Path to the output Inner Nginx configuration file') + parser.add_argument('--outer-config-path', type=Path, + default=Path(os.getenv('NGINX_INIPATH', 'nginx.ini')), + help='Path to the Nginx INI configuration file for outer Nginx') + parser.add_argument('--outer-output-path', type=Path, + default=Path(os.getenv('OUTER_NGINX_OUTPATH', 'outer-nginx.conf')), + help='Path to the output Outer Nginx configuration file') args = parser.parse_args() - if args.docker_compose: - docker_compose_action() - elif args.inner_nginx: - inner_nginx_action() - elif args.outer_nginx: - outer_nginx_action() + if args.all: + docker_compose_action(args.docker_config_path, args.docker_output_path) + inner_nginx_action(args.inner_config_path, args.inner_output_path) + outer_nginx_action(args.outer_config_path, args.outer_output_path) + nginx_reload() else: - # Default behavior: perform all actions - docker_compose_action() - inner_nginx_action() - outer_nginx_action() + if args.docker_compose: + docker_compose_action(args.docker_config_path, args.docker_output_path) + if args.inner_nginx: + inner_nginx_action(args.inner_config_path, args.inner_output_path) + if args.outer_nginx: + outer_nginx_action(args.outer_config_path, args.outer_output_path) + if args.nginx_reload: + nginx_reload() + if args.docker_compose_restart: + docker_compose_restart() + if args.docker_ps: + docker_ps() if __name__ == "__main__": main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..539b7db --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-dotenv +jinja2 diff --git a/src/config_reader.py b/src/config_reader.py index 1afb110..011ad8d 100644 --- a/src/config_reader.py +++ b/src/config_reader.py @@ -1,20 +1,68 @@ +""" +config_reader.py +""" import configparser from dataclasses import dataclass -from typing import Optional +from typing import Optional, List, Tuple from pathlib import Path def read_config(file_path: Path) -> configparser.ConfigParser: - """读取INI配置文件""" + """ + 读取并解析指定路径的 INI 配置文件。 + + 参数: + file_path (Path): 配置文件的路径。 + + 返回: + configparser.ConfigParser: 解析后的配置对象。 + """ config = configparser.ConfigParser() config.read(file_path, encoding='utf-8') return config -def config_get(config: configparser.ConfigParser, section, key): - """从配置文件中获取值,支持默认值""" - return config.get(section, key, fallback=config.get(config.default_section, key, fallback='')) +def config_get(config: configparser.ConfigParser, section: str, key: str, + default: str = None) -> str: + """ + 从配置文件中获取指定键的值,支持默认值。 + + 如果在指定的 section 中找不到键,则尝试从默认 section 中获取。 + + 如果最终值为None则会抛出异常 + + 参数: + config (configparser.ConfigParser): 配置对象。 + section (str): 要查询的 section 名称。 + key (str): 要查询的键名称。 + default (str): 最终配置文件没有时硬编码的默认值。 + + 返回: + str: 查询到的值或默认值。 + """ + res = config.get(section, key, fallback= + config.get(config.default_section, key, fallback=default)) + # if is_raise and res is None: + # raise ValueError(f"Key '{key}' not found in section '{section}' or "\ + # "default section or default value.") + return res @dataclass class NginxConfig: + """ + 表示一个 Nginx 配置项的数据类。 + + 属性: + name (str): 配置项名称,默认为 "unknown"。 + host (str): 主机地址,默认为 "localhost"。 + port (int): 端口号,默认为 80。 + output_path (Optional[str]): 输出路径,默认为 None。 + tcp_nopush (str): TCP 推送选项,默认为 "off"。 + sendfile (str): Sendfile 选项,默认为 "off"。 + client_max_body_size (str): 客户端最大请求体大小,默认为 "100m"。 + proxy_connect_timeout (str): 代理连接超时时间,默认为 "60s"。 + proxy_send_timeout (str): 代理发送超时时间,默认为 "60s"。 + proxy_read_timeout (str): 代理读取超时时间,默认为 "60s"。 + web_socket_proxy (bool): 是否启用 WebSocket 代理,默认为 False。 + """ name: str = "unknown" host: str = "localhost" port: int = 80 @@ -27,70 +75,109 @@ class NginxConfig: proxy_read_timeout: str = "60s" web_socket_proxy: bool = False -def nginx_config(file_path: Path) -> tuple[list[NginxConfig], dict[str, str]]: - """读取INI配置文件并解析为NginxConfig列表""" +def nginx_config(file_path: Path) -> Tuple[List[NginxConfig], dict]: + """ + 读取 INI 配置文件并解析为 NginxConfig 对象列表。 + + 参数: + file_path (Path): 配置文件的路径。 + + 返回: + Tuple[List[NginxConfig], dict]: 包含 NginxConfig 对象的列表和其他相关信息的字典。 + """ config = read_config(file_path) configs = [] g_section = config.default_section - def _config_get(name): - return config_get(config, g_section, name) + def _config_get(name, default = None): + return config_get(config, g_section, name, default) for section in config.sections(): g_section = section configs.append(NginxConfig( - name = section, - host = _config_get('host'), - port = _config_get('port'), - web_socket_proxy = _config_get('web_socket_proxy') == 'True', + name=section, + host=_config_get('host'), + port=int(_config_get('port')), # 将端口转换为整数类型 + web_socket_proxy=_config_get('web_socket_proxy') == 'True', + client_max_body_size=_config_get('client_max_body_size', + NginxConfig.client_max_body_size) )) return (configs, {}) @dataclass class DockerComposeConfig: - name: str + """ + 表示一个 Docker Compose 配置项的数据类。 + + 属性: + name (str): 服务名称。 + container_name (str): 容器名称。 + image (str): 使用的镜像。 + ports (Optional[list[str]]): 映射的端口列表,默认为 None。 + volumes (Optional[list[str]]): 挂载的卷列表,默认为 None。 + environment (Optional[list[str]]): 环境变量列表,默认为 None。 + extra_hosts (Optional[list[str]]): 额外的主机映射列表,默认为 None。 + export (Optional[str]): 导出的端口,默认为 None。 + network (Optional[str]): 所属网络,默认为 None。 + """ + name: str container_name: str - image: str - ports: Optional[list[str]] = None - volumes: Optional[list[str]] = None - environment: Optional[list[str]] = None - extra_hosts: Optional[list[str]] = None + image: str + ports: Optional[List[str]] = None + volumes: Optional[List[str]] = None + environment: Optional[List[str]] = None + extra_hosts: Optional[List[str]] = None + export: Optional[str] = None + network: Optional[str] = None - export: Optional[str] = None - network: Optional[str] = None +def docker_config(file_path: Path) -> Tuple[List[DockerComposeConfig], dict]: + """ + 读取 INI 配置文件并解析为 DockerComposeConfig 对象列表。 -def docker_config(file_path: Path) -> tuple[list[DockerComposeConfig], dict[str, str]]: - """读取INI配置文件并解析为DockerComposeConfig列表""" + 参数: + file_path (Path): 配置文件的路径。 + + 返回: + Tuple[List[DockerComposeConfig], dict]: 包含 DockerComposeConfig 对象的列表和其他相关信息的字典。 + """ config = read_config(file_path) configs = [] g_section = '' - def _config_get(name): - return config_get(config, g_section, name) + def _config_get(name, default = None): + return config_get(config, g_section, name, default) + + def _get_item(name) -> List[str]: + """将逗号分隔的字符串转换为列表,并去除空格和空字符串。""" + return [i.strip() for i in _config_get(name, '').split(',') if i.strip()] - def _get_item(name) -> list[str]: - return [i.strip() for i in _config_get(name).split(',') if i.strip()] - network = set() for section in config.sections(): g_section = section configs.append(DockerComposeConfig( - name = section, - container_name = _config_get('container_name'), - image = _config_get('image'), - volumes = _get_item('volumes'), - ports = _get_item('ports'), - environment = _get_item('environment'), - extra_hosts = _get_item('extra_hosts'), - export = _config_get('export'), - network = _config_get('network'), + name=section, + container_name=_config_get('container_name'), + image=_config_get('image'), + volumes=_get_item('volumes'), + ports=_get_item('ports'), + environment=_get_item('environment'), + extra_hosts=_get_item('extra_hosts'), + export=_config_get('export'), + network=_config_get('network'), )) network.add(_config_get('network')) return (configs, {'networks': list(network)}) -def docker2nginx(file_path: Path) -> tuple[list[NginxConfig], dict[str, str]]: - "将 docker-compose 服务转换为 nginx 代理" - """读取INI配置文件并解析为DockerComposeConfig列表""" +def docker2nginx(file_path: Path) -> Tuple[List[NginxConfig], dict]: + """ + 将 Docker Compose 服务配置转换为 Nginx 代理配置。 + + 参数: + file_path (Path): 配置文件的路径。 + + 返回: + Tuple[List[NginxConfig], dict]: 包含 NginxConfig 对象的列表和其他相关信息的字典。 + """ config = read_config(file_path) configs = [] @@ -98,18 +185,17 @@ def docker2nginx(file_path: Path) -> tuple[list[NginxConfig], dict[str, str]]: def _config_get(name): return config_get(config, g_section, name) - def _get_item(name) -> list[str]: + def _get_item(name) -> List[str]: + """将逗号分隔的字符串转换为列表,并去除空格和空字符串。""" return [i.strip() for i in _config_get(name).split(',') if i.strip()] - - network = set() + for section in config.sections(): - if (section == 'nginx'): + if section == 'nginx': continue g_section = section configs.append(NginxConfig( - name = config.get(section, 'nginx_name', fallback=section), - host = _config_get('container_name'), - port = _config_get('export'), + name=config.get(section, 'nginx_name', fallback=section), + host=_config_get('container_name'), + port=_config_get('export'), )) - network.add(_config_get('network')) - return (configs, {'networks': list(network)}) + return (configs, {}) diff --git a/src/jinja2_render.py b/src/jinja2_render.py index abc3a68..5a6c655 100644 --- a/src/jinja2_render.py +++ b/src/jinja2_render.py @@ -1,8 +1,12 @@ +""" +jinja2_render.py +""" from pathlib import Path from dataclasses import asdict, dataclass from jinja2 import Environment, FileSystemLoader, Template -def render_dataclass(configs: list[dataclass], tmp_path: Path, out_path: Path, other = None) -> str: +def render_dataclass(configs: list[dataclass], tmp_path: Path, out_path: Path, + verbose :bool = True, other = None) -> str: "from dataclasses to jinja template" env = Environment(loader=FileSystemLoader(str(tmp_path.parent), encoding='utf-8')) @@ -10,4 +14,6 @@ def render_dataclass(configs: list[dataclass], tmp_path: Path, out_path: Path, o data = [asdict(config) for config in configs] res = template.render({'autoconfigs': data}, **other) out_path.write_text(res, encoding='utf-8') + if verbose: + print(f"{out_path.absolute()} is rendered") return res