From a4ebe33fcaa0e55986cf6a361401a31897aff480 Mon Sep 17 00:00:00 2001
From: ZZY <2450266535@qq.com>
Date: Sat, 1 Feb 2025 14:47:38 +0800
Subject: [PATCH] =?UTF-8?q?feat(main):=20=E5=A2=9E=E5=8A=A0=E6=96=B0?=
 =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E7=8E=B0=E6=9C=89?=
 =?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- 添加了新的命令行参数和功能:
  - --nginx-reload: 重新加载 Nginx 配置
  - --docker-compose-restart: 重启 Docker Compose 服务
  - --docker-ps: 列出运行中的 Docker 容器
- 重构了 main 函数,使其支持更灵活的参数配置
- 优化了配置文件读取和解析逻辑
- 更新了 .gitignore 文件,添加了 requirements.txt
- 修正了拼写错误:INNER_NGINX_OUTPATH -> inner-nginx.conf
---
 .gitignore           |   3 +-
 defaultenv           |   2 +-
 main.py              | 102 +++++++++++++++++-------
 requirements.txt     |   2 +
 src/config_reader.py | 184 +++++++++++++++++++++++++++++++------------
 src/jinja2_render.py |   8 +-
 6 files changed, 222 insertions(+), 79 deletions(-)
 create mode 100644 requirements.txt

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