|
| 1 | +import os |
| 2 | +import json |
| 3 | +import subprocess |
| 4 | +from jinja2 import Environment, FileSystemLoader |
| 5 | + |
| 6 | +# --- Configuration --- |
| 7 | +TEMPLATE_DIR = '/app/config-generator/templates' |
| 8 | +PROXY_OUTPUT_DIR = '/config/nginx/proxy-confs' |
| 9 | +DEFAULT_CONF_OUTPUT = '/config/nginx/site-confs/default.conf' |
| 10 | +HTPASSWD_FILE = '/config/nginx/.htpasswd' |
| 11 | +# --------------------- |
| 12 | + |
| 13 | +def process_service_config(service_name, service_config_json, global_auth_provider, auth_exclude_list): |
| 14 | + """Processes a single service configuration, including auth logic.""" |
| 15 | + service_config = json.loads(service_config_json) |
| 16 | + |
| 17 | + # The default service doesn't have a subdomain name in the traditional sense |
| 18 | + if service_name.lower() == 'default': |
| 19 | + # We still need a target container name, let the user define it or raise an error |
| 20 | + if 'name' not in service_config: |
| 21 | + raise ValueError("PROXY_CONFIG_DEFAULT must contain a 'name' key specifying the target container name.") |
| 22 | + else: |
| 23 | + service_config['name'] = service_name |
| 24 | + |
| 25 | + # --- Authentication Logic --- |
| 26 | + auth_provider = 'none' # Default |
| 27 | + # 1. Per-service override |
| 28 | + if 'auth' in service_config: |
| 29 | + auth_provider = service_config['auth'] |
| 30 | + print(f" - Found per-service auth override: '{auth_provider}'") |
| 31 | + # 2. Global provider check |
| 32 | + elif global_auth_provider and service_name not in auth_exclude_list: |
| 33 | + auth_provider = global_auth_provider |
| 34 | + print(f" - Applying global auth provider: '{auth_provider}'") |
| 35 | + # 3. Otherwise, no auth |
| 36 | + else: |
| 37 | + if service_name in auth_exclude_list: |
| 38 | + print(f" - Service is in global exclude list. No auth.") |
| 39 | + else: |
| 40 | + print(f" - No auth provider specified.") |
| 41 | + |
| 42 | + service_config['auth_provider'] = auth_provider |
| 43 | + return service_config |
| 44 | + |
| 45 | +def generate_configs(): |
| 46 | + """ |
| 47 | + Generates Nginx config files from PROXY_CONFIG environment variables and a Jinja2 template. |
| 48 | + """ |
| 49 | + print("--- Starting Nginx Config Generation from Environment Variables ---") |
| 50 | + |
| 51 | + # Ensure output directories exist |
| 52 | + os.makedirs(PROXY_OUTPUT_DIR, exist_ok=True) |
| 53 | + os.makedirs(os.path.dirname(DEFAULT_CONF_OUTPUT), exist_ok=True) |
| 54 | + print(f"Output directories are ready.") |
| 55 | + |
| 56 | + # Get global auth settings from environment variables |
| 57 | + global_auth_provider = os.environ.get('PROXY_AUTH_PROVIDER') |
| 58 | + auth_exclude_list = os.environ.get('PROXY_AUTH_EXCLUDE', '').split(',') |
| 59 | + auth_exclude_list = [name.strip() for name in auth_exclude_list if name.strip()] |
| 60 | + |
| 61 | + # Get basic auth credentials |
| 62 | + basic_auth_user = os.environ.get('PROXY_AUTH_BASIC_USER') |
| 63 | + basic_auth_pass = os.environ.get('PROXY_AUTH_BASIC_PASS') |
| 64 | + basic_auth_configured = False |
| 65 | + |
| 66 | + print(f"Global Auth Provider: {global_auth_provider}") |
| 67 | + print(f"Auth Exclude List: {auth_exclude_list}") |
| 68 | + |
| 69 | + # Collect and process service configurations |
| 70 | + subdomain_services = [] |
| 71 | + default_service = None |
| 72 | + |
| 73 | + for key, value in os.environ.items(): |
| 74 | + if key.startswith('PROXY_CONFIG_'): |
| 75 | + service_name = key.replace('PROXY_CONFIG_', '').lower() |
| 76 | + print(f" Processing service: {service_name}") |
| 77 | + print(value) |
| 78 | + try: |
| 79 | + service_config = process_service_config(service_name, value, global_auth_provider, auth_exclude_list) |
| 80 | + |
| 81 | + # Handle Basic Auth File Creation |
| 82 | + if service_config['auth_provider'] == 'basic' and not basic_auth_configured: |
| 83 | + if basic_auth_user and basic_auth_pass: |
| 84 | + print(f" - Configuring Basic Auth with user '{basic_auth_user}'.") |
| 85 | + try: |
| 86 | + os.makedirs(os.path.dirname(HTPASSWD_FILE), exist_ok=True) |
| 87 | + command = ['htpasswd', '-bc', HTPASSWD_FILE, basic_auth_user, basic_auth_pass] |
| 88 | + subprocess.run(command, check=True, capture_output=True, text=True) |
| 89 | + print(f" - Successfully created '{HTPASSWD_FILE}'.") |
| 90 | + basic_auth_configured = True |
| 91 | + except subprocess.CalledProcessError as e: |
| 92 | + print(f" [!!] ERROR: 'htpasswd' command failed: {e.stderr}. Basic auth will not be enabled.") |
| 93 | + service_config['auth_provider'] = 'none' |
| 94 | + except FileNotFoundError: |
| 95 | + print(f" [!!] ERROR: 'htpasswd' command not found. Basic auth will not be enabled.") |
| 96 | + service_config['auth_provider'] = 'none' |
| 97 | + else: |
| 98 | + print(f" [!!] WARNING: 'auth: basic' is set, but PROXY_AUTH_BASIC_USER or PROXY_AUTH_BASIC_PASS is missing. Skipping auth.") |
| 99 | + service_config['auth_provider'] = 'none' |
| 100 | + |
| 101 | + if service_name == 'default': |
| 102 | + default_service = service_config |
| 103 | + else: |
| 104 | + subdomain_services.append(service_config) |
| 105 | + |
| 106 | + except (json.JSONDecodeError, ValueError) as e: |
| 107 | + print(f" [!!] ERROR: Could not parse or validate config for {service_name}: {e}. Skipping.") |
| 108 | + except Exception as e: |
| 109 | + print(f" [!!] ERROR: An unexpected error occurred processing {service_name}: {e}. Skipping.") |
| 110 | + |
| 111 | + # Set up Jinja2 environment |
| 112 | + try: |
| 113 | + env = Environment(loader=FileSystemLoader(TEMPLATE_DIR), trim_blocks=True, lstrip_blocks=True) |
| 114 | + proxy_template = env.get_template('proxy.conf.j2') |
| 115 | + default_template = env.get_template('default.conf.j2') |
| 116 | + print("\nJinja2 templates loaded successfully.") |
| 117 | + except Exception as e: |
| 118 | + print(f"ERROR: Failed to load Jinja2 templates from '{TEMPLATE_DIR}': {e}. Exiting.") |
| 119 | + return |
| 120 | + |
| 121 | + # Generate default site config if specified |
| 122 | + if default_service: |
| 123 | + print("\n--- Generating Default Site Config ---") |
| 124 | + try: |
| 125 | + rendered_content = default_template.render(item=default_service) |
| 126 | + with open(DEFAULT_CONF_OUTPUT, 'w') as f: |
| 127 | + f.write(rendered_content) |
| 128 | + print(f" [OK] Generated {os.path.basename(DEFAULT_CONF_OUTPUT)}") |
| 129 | + except Exception as e: |
| 130 | + print(f" [!!] ERROR: Failed to render or write default config: {e}") |
| 131 | + else: |
| 132 | + print("\n--- PROXY_CONFIG_DEFAULT not set, default site config will not be generated. ---") |
| 133 | + |
| 134 | + |
| 135 | + # Generate subdomain proxy configs |
| 136 | + print("\n--- Generating Subdomain Proxy Configs ---") |
| 137 | + if not subdomain_services: |
| 138 | + print("No subdomain services found to configure.") |
| 139 | + for service in subdomain_services: |
| 140 | + filename = f"{service['name']}.subdomain.conf" |
| 141 | + output_path = os.path.join(PROXY_OUTPUT_DIR, filename) |
| 142 | + try: |
| 143 | + rendered_content = proxy_template.render(item=service) |
| 144 | + with open(output_path, 'w') as f: |
| 145 | + f.write(rendered_content) |
| 146 | + print(f" [OK] Generated {filename}") |
| 147 | + except Exception as e: |
| 148 | + print(f" [!!] ERROR: Failed to render or write config for {service['name']}: {e}") |
| 149 | + |
| 150 | + print("\n--- Generation Complete ---") |
| 151 | + |
| 152 | +if __name__ == "__main__": |
| 153 | + generate_configs() |
0 commit comments