dify/docker/generate_docker_compose
macat 48d27e250b
refactor: split docker-compose env config into separate files (#31586)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
2026-05-08 15:36:20 +00:00

161 lines
6.0 KiB
Python
Executable File

#!/usr/bin/env python3
import os
import re
import sys
# Variables that exist only for Docker Compose orchestration and must NOT be
# injected into containers as environment variables.
SHARED_ENV_EXCLUDE = frozenset(
[
# Docker Compose profile selection
"COMPOSE_PROFILES",
# Worker health check orchestration flags (consumed by docker-compose,
# not by the application running inside the container)
"COMPOSE_WORKER_HEALTHCHECK_DISABLED",
"COMPOSE_WORKER_HEALTHCHECK_INTERVAL",
"COMPOSE_WORKER_HEALTHCHECK_TIMEOUT",
]
)
def parse_env_example(file_path):
"""
Parses the .env.example file and returns a dictionary with variable names as keys and default values as values.
"""
env_vars = {}
with open(file_path, "r", encoding="utf-8") as f:
for line_number, line in enumerate(f, 1):
line = line.strip()
# Ignore empty lines and comments
if not line or line.startswith("#"):
continue
# Use regex to parse KEY=VALUE
match = re.match(r"^([^=]+)=(.*)$", line)
if match:
key = match.group(1).strip()
value = match.group(2).strip()
# Remove possible quotes around the value
if (value.startswith('"') and value.endswith('"')) or (
value.startswith("'") and value.endswith("'")
):
value = value[1:-1]
env_vars[key] = value
else:
print(f"Warning: Unable to parse line {line_number}: {line}")
return env_vars
def generate_shared_env_block(env_vars, anchor_name="shared-api-worker-env"):
"""
Generates a shared environment variables block as a YAML string.
"""
lines = [f"x-shared-env: &{anchor_name}"]
for key, default in env_vars.items():
if key in SHARED_ENV_EXCLUDE:
continue
# If default value is empty, use ${KEY:-}
if default == "":
lines.append(f" {key}: ${{{key}:-}}")
else:
# If default value contains special characters, wrap it in quotes
if re.search(r"[:\s]", default):
default = f"{default}"
lines.append(f" {key}: ${{{key}:-{default}}}")
return "\n".join(lines)
def create_env_files_from_example(env_example_path):
"""
Creates actual env files from .env.example by copying the categorized .env.example files.
This allows docker-compose to use env_file references.
Supports per-module structure with subdirectories.
"""
base_dir = os.path.dirname(os.path.abspath(env_example_path))
root_env_file = os.path.join(base_dir, ".env")
if not os.path.exists(root_env_file):
with open(env_example_path, "r", encoding="utf-8") as src, open(
root_env_file, "w", encoding="utf-8", newline="\n"
) as dst:
dst.write(src.read())
print(f"Created {root_env_file}")
else:
print(f"{root_env_file} already exists, skipping")
envs_dir = os.path.join(base_dir, "envs")
if not os.path.isdir(envs_dir):
print(f"No envs directory found at {envs_dir}, skipping split env files")
return []
created_files = []
# Walk through all .env.example files in subdirectories
for root, dirs, files in os.walk(envs_dir):
for file in files:
if file.endswith('.env.example'):
example_file = os.path.join(root, file)
env_file = example_file.replace('.env.example', '.env')
if os.path.exists(env_file):
print(f"{env_file} already exists, skipping")
continue
# Copy .example to actual file
with open(example_file, "r", encoding="utf-8") as src, open(
env_file, "w", encoding="utf-8", newline="\n"
) as dst:
dst.write(src.read())
created_files.append(env_file)
print(f"Created {env_file}")
return created_files
def insert_shared_env(template_path, output_path, header_comments):
"""
Copies the template file to output path with header comments.
The template now uses env_file references instead of a huge YAML anchor.
"""
with open(template_path, "r", encoding="utf-8") as f:
template_content = f.read()
# Prepare the final content with header comments
final_content = f"{header_comments}\n{template_content}"
with open(output_path, "w", encoding="utf-8", newline="\n") as f:
f.write(final_content)
print(f"Generated {output_path}")
def main():
base_dir = os.path.dirname(os.path.abspath(__file__))
env_example_path = os.path.join(base_dir, ".env.example")
template_path = os.path.join(base_dir, "docker-compose-template.yaml")
output_path = os.path.join(base_dir, "docker-compose.yaml")
# Define header comments to be added at the top of docker-compose.yaml
header_comments = (
"# ==================================================================\n"
"# WARNING: This file is auto-generated by generate_docker_compose\n"
"# Do not modify this file directly. Instead, update the .env.example\n"
"# or docker-compose-template.yaml and regenerate this file.\n"
"# ==================================================================\n"
)
# Check if required files exist
for path in [env_example_path, template_path]:
if not os.path.isfile(path):
print(f"Error: File {path} does not exist.")
sys.exit(1)
# Create env files from categorized .env.example files
# These files are used by docker-compose's env_file directive
# This ensures .env files exist even in CI/CD environments
create_env_files_from_example(env_example_path)
# Copy template to output with header comments
# The template now uses env_file references instead of a huge YAML anchor
insert_shared_env(template_path, output_path, header_comments)
if __name__ == "__main__":
main()