mirror of
https://github.com/nickyeoman/docker-compose-cookbooks.git
synced 2026-09-03 18:36:22 +00:00
283 lines
9.0 KiB
Python
283 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
import os
|
|
import json
|
|
import re
|
|
import requests
|
|
from pathlib import Path
|
|
|
|
# ---------------------------------------------------------
|
|
# Paths
|
|
# ---------------------------------------------------------
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
REPO_ROOT = SCRIPT_DIR.parent
|
|
OUTPUT_FILE = REPO_ROOT / "templates.json"
|
|
|
|
# ---------------------------------------------------------
|
|
# Extract "Overview" section from README.md (strict)
|
|
# ---------------------------------------------------------
|
|
def extract_overview(readme_path: Path, project_name: str) -> str:
|
|
if not readme_path.exists():
|
|
return f"{project_name} description to come"
|
|
|
|
lines = readme_path.read_text().splitlines()
|
|
overview = []
|
|
in_section = False
|
|
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
if stripped == "## Overview":
|
|
in_section = True
|
|
continue
|
|
if in_section:
|
|
if stripped.startswith("#"):
|
|
break
|
|
if stripped:
|
|
overview.append(stripped)
|
|
|
|
if not overview:
|
|
return f"{project_name} description to come"
|
|
|
|
return " ".join(overview)
|
|
|
|
# ---------------------------------------------------------
|
|
# Logo URL mapping with existence check
|
|
# ---------------------------------------------------------
|
|
def find_logo_url(dir_path: Path, nologo: bool = False) -> str:
|
|
base_url = "https://i.4lt.ca/cookbooks/"
|
|
name = dir_path.name
|
|
logo_url = f"{base_url}{name}.png"
|
|
|
|
if nologo:
|
|
return f"{base_url}default.png"
|
|
|
|
try:
|
|
resp = requests.head(logo_url, timeout=5)
|
|
if resp.status_code == 200:
|
|
return logo_url
|
|
except requests.RequestException:
|
|
pass
|
|
|
|
return f"{base_url}default.png"
|
|
|
|
# ---------------------------------------------------------
|
|
# Parse image from docker-compose.yml using sample.env defaults
|
|
# ---------------------------------------------------------
|
|
def parse_image(compose_path: Path, env_vars: list) -> str:
|
|
if not compose_path.exists():
|
|
return ""
|
|
|
|
env_defaults = {v["name"]: v["default"] for v in env_vars}
|
|
|
|
for line in compose_path.read_text().splitlines():
|
|
line = line.strip()
|
|
if line.startswith("image:"):
|
|
image = line.split("image:", 1)[1].strip().strip('"').strip("'")
|
|
|
|
# Handle ${VAR:-default} or ${VAR}
|
|
def replace_var(match):
|
|
var_name = match.group(1)
|
|
bash_default = match.group(2)
|
|
return env_defaults.get(var_name, bash_default if bash_default else "")
|
|
|
|
image = re.sub(r"\$\{([^:}]+)(?:[:-]([^}]+))?\}", replace_var, image)
|
|
return image
|
|
|
|
return ""
|
|
|
|
# ---------------------------------------------------------
|
|
# Parse ports from docker-compose.yml
|
|
# Returns a list of dicts: {"container": port, "published": port, "protocol": "tcp"}
|
|
# ---------------------------------------------------------
|
|
def parse_ports(compose_path: Path):
|
|
if not compose_path.exists():
|
|
return []
|
|
|
|
ports_list = []
|
|
lines = compose_path.read_text().splitlines()
|
|
in_ports = False
|
|
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
if stripped.startswith("ports:"):
|
|
in_ports = True
|
|
continue
|
|
if in_ports:
|
|
if not stripped or not stripped.startswith("-"):
|
|
break
|
|
|
|
port_str = stripped[1:].strip().split("#", 1)[0].strip().strip('"').strip("'")
|
|
if not port_str:
|
|
continue
|
|
|
|
protocol = "tcp"
|
|
if "/" in port_str:
|
|
port_part, protocol_part = port_str.split("/", 1)
|
|
protocol = protocol_part.strip()
|
|
else:
|
|
port_part = port_str
|
|
|
|
if ":" in port_part:
|
|
host_port, container_port = port_part.split(":", 1)
|
|
else:
|
|
container_port = port_part
|
|
host_port = container_port
|
|
|
|
try:
|
|
container_port = int(container_port.strip())
|
|
host_port = int(host_port.strip())
|
|
except ValueError:
|
|
continue
|
|
|
|
ports_list.append({
|
|
"container": container_port,
|
|
"published": host_port,
|
|
"protocol": protocol
|
|
})
|
|
|
|
return ports_list
|
|
|
|
# ---------------------------------------------------------
|
|
# Parse volumes from docker-compose.yml
|
|
# Returns a list of dicts: {"name": "unique-name", "container": "/container/path"}
|
|
# ---------------------------------------------------------
|
|
def parse_volumes(compose_path: Path, stack_name: str):
|
|
"""
|
|
Parse volumes from docker-compose.yml and generate Portainer-ready entries.
|
|
Host paths and VOL_PATH references are ignored; only container paths are used.
|
|
"""
|
|
if not compose_path.exists():
|
|
return []
|
|
|
|
volumes = []
|
|
lines = compose_path.read_text().splitlines()
|
|
in_volumes = False
|
|
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
if stripped.startswith("volumes:"):
|
|
in_volumes = True
|
|
continue
|
|
if in_volumes:
|
|
if not stripped or not stripped.startswith("-"):
|
|
break
|
|
|
|
# Strip "- ", quotes, inline comments
|
|
volume_str = stripped[1:].strip().split("#", 1)[0].strip().strip('"').strip("'")
|
|
if not volume_str:
|
|
continue
|
|
|
|
# Split host:container[:ro|rw]
|
|
parts = volume_str.split(":")
|
|
if len(parts) == 1:
|
|
container_path = parts[0].strip()
|
|
else:
|
|
container_path = parts[-1].strip() # always take the last part as container path
|
|
|
|
# Remove :ro or :rw suffix if present
|
|
container_path = re.sub(r":(ro|rw)$", "", container_path)
|
|
|
|
# Generate unique name for Portainer from stack + container path
|
|
name = f"{stack_name}-{container_path.strip('/').replace('/', '-')}"
|
|
if not name:
|
|
name = f"{stack_name}-volume"
|
|
|
|
volumes.append({"name": name, "container": container_path})
|
|
|
|
return volumes
|
|
|
|
# ---------------------------------------------------------
|
|
# Parse environment variables from sample.env or env-sample
|
|
# Returns a list of dicts: {"name": ..., "default": ...}
|
|
# ---------------------------------------------------------
|
|
def parse_env_vars(dir_path: Path):
|
|
env_file = None
|
|
if (dir_path / "sample.env").exists():
|
|
env_file = dir_path / "sample.env"
|
|
elif (dir_path / "env-sample").exists():
|
|
env_file = dir_path / "env-sample"
|
|
|
|
if not env_file:
|
|
return []
|
|
|
|
env_list = []
|
|
for line in env_file.read_text().splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if "=" in line:
|
|
key, value = line.split("=", 1)
|
|
key = key.strip()
|
|
value = value.split("#", 1)[0].strip().strip('"').strip("'")
|
|
if key:
|
|
env_list.append({"name": key, "default": value})
|
|
|
|
return env_list
|
|
|
|
# ---------------------------------------------------------
|
|
# Build template object for a stack directory
|
|
# ---------------------------------------------------------
|
|
def generate_template_object(dir_path: Path, nologo: bool = False):
|
|
name = dir_path.name
|
|
compose_file = dir_path / "docker-compose.yml"
|
|
readme_file = dir_path / "README.md"
|
|
|
|
description = extract_overview(readme_file, name)
|
|
logo = find_logo_url(dir_path, nologo=False)
|
|
env_vars = parse_env_vars(dir_path)
|
|
image = parse_image(compose_file, env_vars)
|
|
ports = parse_ports(compose_file)
|
|
volumes = parse_volumes(compose_file, name)
|
|
|
|
env_entries = [
|
|
{"name": v["name"], "label": v["name"], "default": v["default"]}
|
|
for v in env_vars if v["default"] is not None
|
|
]
|
|
|
|
return {
|
|
"type": 1,
|
|
"title": name,
|
|
"description": description,
|
|
"note": "Auto-generated template from repository",
|
|
"categories": ["Auto"],
|
|
"platform": "linux",
|
|
"logo": logo,
|
|
"image": image,
|
|
"repository": {
|
|
"url": "https://github.com/nickyeoman/docker-compose-cookbooks",
|
|
"stackfile": f"{name}/docker-compose.yml",
|
|
"stackfile_plain": False
|
|
},
|
|
"env": env_entries,
|
|
"ports": ports,
|
|
"volumes": volumes
|
|
}
|
|
|
|
# ---------------------------------------------------------
|
|
# Main script logic
|
|
# ---------------------------------------------------------
|
|
def main():
|
|
templates = []
|
|
|
|
for item in REPO_ROOT.iterdir():
|
|
if not item.is_dir():
|
|
continue
|
|
if item.name.startswith("_"):
|
|
continue
|
|
if item.name.endswith(("_dev", "_notes")):
|
|
continue
|
|
if not (item / "docker-compose.yml").exists():
|
|
continue
|
|
|
|
templates.append(generate_template_object(item, nologo=True))
|
|
|
|
output_data = {
|
|
"version": "3",
|
|
"templates": templates
|
|
}
|
|
|
|
OUTPUT_FILE.write_text(json.dumps(output_data, indent=2))
|
|
print(f"Generated {OUTPUT_FILE} with templates for Docker Compose stacks.")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|