commit 3b9dac3b7f1b130ba3d2e924643479121613e376 Author: ZZY <2450266535@qq.com> Date: Mon Nov 11 13:31:20 2024 +0800 feat: 初始化项目结构和基本功能 - 创建项目目录结构和主要文件 - 实现 Docker 和 Nginx 配置生成的基本功能 - 添加命令行参数解析和默认行为逻辑 - 实现配置文件读取和解析功能 - 添加 Jinja2 模板渲染功能 - 创建 Nginx 和 Docker Compose 配置模板 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eba624a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +* +!src/ +!src/* +!.gitignore +!default* +!main.py \ No newline at end of file diff --git a/defaultenv b/defaultenv new file mode 100644 index 0000000..41c5d57 --- /dev/null +++ b/defaultenv @@ -0,0 +1,6 @@ +DOCKER_INIPATH = docker.ini +NGINX_INIPATH = nginx.ini + +DOCKER_COMPOSE_OUTPATH = docker-compose.yml +INNER_NGINX_OUTPATH = innner-nginx.conf +OUTER_NGINX_OUTPATH = outer-nginx.conf \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..eec78c2 --- /dev/null +++ b/main.py @@ -0,0 +1,68 @@ +""" +this is the main file +""" +import argparse +import os +import subprocess +from pathlib import Path +from dotenv import load_dotenv +from jinja2_render import render_dataclass +from src.config_reader import nginx_config, docker_config, docker2nginx + +load_dotenv() + +def run_command(command): + try: + result = subprocess.run(command, shell=True, check=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + print(result.stdout.decode()) + except subprocess.CalledProcessError as e: + print(f"Command failed with error: {e.stderr.decode()}") + +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')) + + (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')) + + (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')) + + (auto_config, other) = nginx_config(config_path) + render_dataclass(auto_config, Path('./src/nginx-conf.j2'), output_path, other) + +def main(): + + parser = argparse.ArgumentParser(description="Automate Docker and Nginx configuration.") + 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)') + + 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() + else: + # Default behavior: perform all actions + docker_compose_action() + inner_nginx_action() + outer_nginx_action() + +if __name__ == "__main__": + main() diff --git a/src/config_reader.py b/src/config_reader.py new file mode 100644 index 0000000..1afb110 --- /dev/null +++ b/src/config_reader.py @@ -0,0 +1,115 @@ +import configparser +from dataclasses import dataclass +from typing import Optional +from pathlib import Path + +def read_config(file_path: Path) -> configparser.ConfigParser: + """读取INI配置文件""" + 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='')) + +@dataclass +class NginxConfig: + name: str = "unknown" + host: str = "localhost" + port: int = 80 + output_path: Optional[str] = None + tcp_nopush: str = "off" + sendfile: str = "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 = False + +def nginx_config(file_path: Path) -> tuple[list[NginxConfig], dict[str, str]]: + """读取INI配置文件并解析为NginxConfig列表""" + config = read_config(file_path) + configs = [] + + g_section = config.default_section + def _config_get(name): + return config_get(config, g_section, name) + + 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', + )) + return (configs, {}) + +@dataclass +class DockerComposeConfig: + 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 + +def docker_config(file_path: Path) -> tuple[list[DockerComposeConfig], dict[str, str]]: + """读取INI配置文件并解析为DockerComposeConfig列表""" + config = read_config(file_path) + configs = [] + + g_section = '' + def _config_get(name): + return config_get(config, g_section, name) + + 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'), + )) + 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列表""" + config = read_config(file_path) + configs = [] + + g_section = '' + def _config_get(name): + return config_get(config, g_section, name) + + 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'): + continue + g_section = section + configs.append(NginxConfig( + 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)}) diff --git a/src/docker-compose.j2 b/src/docker-compose.j2 new file mode 100644 index 0000000..f9104ba --- /dev/null +++ b/src/docker-compose.j2 @@ -0,0 +1,55 @@ +# docker-compose.j2 +version: '3' + +services: + {%- for config in autoconfigs %} + {{ config.name }}: + container_name: {{ config.container_name }} + image: {{ config.image }} + deploy: + update_config: + parallelism: 1 + delay: 10s + restart_policy: + condition: on-failure + max_attempts: 3 + {%- if config.ports %} + ports: + {%- for port in config.ports %} + - {{ port }} + {%- endfor %} + {%- endif %} + {%- if config.volumes %} + volumes: + {%- for volume in config.volumes %} + - {{ volume }} + {%- endfor %} + {%- endif %} + {%- if config.environment %} + environment: + {%- for env in config.environment %} + - {{ env }} + {%- endfor %} + {%- endif %} + {%- if config.export %} + expose: + - {{ config.export }} + {%- endif %} + {%- if config.extra_hosts %} + extra_hosts: + {%- for host in config.extra_hosts %} + - {{ host }} + {%- endfor %} + {%- endif %} + {%- if config.network %} + networks: + - {{ config.network }} + {%- endif %} + {% endfor %} + +{%- if networks %} +networks: +{%- for network in networks %} + {{ network }}: +{%- endfor %} +{%- endif %} \ No newline at end of file diff --git a/src/jinja2_render.py b/src/jinja2_render.py new file mode 100644 index 0000000..abc3a68 --- /dev/null +++ b/src/jinja2_render.py @@ -0,0 +1,13 @@ +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: + "from dataclasses to jinja template" + env = Environment(loader=FileSystemLoader(str(tmp_path.parent), + encoding='utf-8')) + template: Template = env.get_template(str(tmp_path.name)) + data = [asdict(config) for config in configs] + res = template.render({'autoconfigs': data}, **other) + out_path.write_text(res, encoding='utf-8') + return res diff --git a/src/nginx-conf.j2 b/src/nginx-conf.j2 new file mode 100644 index 0000000..e85f35c --- /dev/null +++ b/src/nginx-conf.j2 @@ -0,0 +1,28 @@ +{%- for config in autoconfigs %} +server { + listen 80; + listen 443 ssl; + server_name {{ config.name }}; + if ($scheme = http) { + return 301 https://$server_name$request_uri; + } + + tcp_nopush {{ config.tcp_nopush }}; + sendfile {{ config.sendfile }}; + client_max_body_size {{ config.client_max_body_size }}; + location / { + proxy_pass http://{{ config.host }}:{{ config.port }}; + proxy_connect_timeout {{ config.proxy_connect_timeout}}; + proxy_send_timeout {{ config.proxy_send_timeout }}; + proxy_read_timeout {{ config.proxy_read_timeout }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + {%- if config.web_socket_proxy %} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + {%- endif %} + } +} +{% endfor %} \ No newline at end of file