mirror of
https://github.com/nickyeoman/docker-compose-cookbooks.git
synced 2026-09-04 02:46:23 +00:00
199 lines
6.8 KiB
Python
199 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Portainer templates generator (Portainer CE compatible)
|
|
Usage:
|
|
python3 _build/portainer_template_build.py [--nologo] [--branch BRANCH] [--output PATH] [--no-skip-volpath]
|
|
Defaults:
|
|
branch = master
|
|
output = ./templates.json
|
|
skips projects containing "${VOL_PATH" by default (use --no-skip-volpath to disable)
|
|
Note: Branch is used only for logo asset lookup; templates use default repo branch (master).
|
|
"""
|
|
from pathlib import Path
|
|
import argparse
|
|
import json
|
|
import re
|
|
import requests
|
|
import sys
|
|
from typing import List, Dict
|
|
# -------------------------
|
|
# Config / Defaults
|
|
# -------------------------
|
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
REPO_ROOT = SCRIPT_DIR.parent
|
|
DEFAULT_OUTPUT = REPO_ROOT / "templates.json"
|
|
ASSETS_BASE = "https://i.4lt.ca/cookbooks"
|
|
GITHUB_RAW_BASE_TEMPLATE = "https://raw.githubusercontent.com/{owner}/{repo}/{branch}"
|
|
GITHUB_OWNER = "nickyeoman"
|
|
GITHUB_REPO = "docker-compose-cookbooks"
|
|
DEFAULT_BRANCH = "master"
|
|
SKIP_VOLPATH_PATTERN = "${VOL_PATH"
|
|
# Regex
|
|
VAR_RE = re.compile(r"\$\{([^:}]+)(?:[:-]([^}]+))?\}")
|
|
# -------------------------
|
|
# Helpers
|
|
# -------------------------
|
|
def read_text_safe(p: Path) -> str:
|
|
try:
|
|
return p.read_text()
|
|
except Exception:
|
|
return ""
|
|
def write_atomic(path: Path, data: str):
|
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
tmp.write_text(data)
|
|
tmp.replace(path)
|
|
def slugify_for_name(s: str) -> str:
|
|
# produce a safe component for volume names etc.
|
|
s = s.strip().lower()
|
|
s = re.sub(r"[^a-z0-9_\-]+", "-", s)
|
|
s = re.sub(r"-{2,}", "-", s)
|
|
s = s.strip("-")
|
|
if not s:
|
|
return "x"
|
|
return s
|
|
# -------------------------
|
|
# README Overview extraction
|
|
# -------------------------
|
|
def extract_overview(readme_path: Path, project_name: str) -> str:
|
|
text = read_text_safe(readme_path)
|
|
if not text:
|
|
return f"{project_name} description to come"
|
|
lines = 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 lookup (HEAD check)
|
|
# -------------------------
|
|
def find_logo_url(dir_path: Path, nologo: bool, branch: str) -> str:
|
|
name = dir_path.name
|
|
# Use branch for asset if needed, but since assets are static, ignore branch here
|
|
logo = f"{ASSETS_BASE}/{name}.png"
|
|
default = f"{ASSETS_BASE}/default.png"
|
|
if nologo:
|
|
return default
|
|
try:
|
|
r = requests.head(logo, timeout=4)
|
|
if r.status_code == 200:
|
|
return logo
|
|
except Exception:
|
|
pass
|
|
return default
|
|
# -------------------------
|
|
# Parse environment variables from sample.env/env-sample
|
|
# -------------------------
|
|
def parse_env_vars(dir_path: Path) -> List[Dict[str,str]]:
|
|
candidates = [dir_path / "sample.env", dir_path / "env-sample"]
|
|
env_file = None
|
|
for c in candidates:
|
|
if c.exists():
|
|
env_file = c
|
|
break
|
|
if not env_file:
|
|
return []
|
|
out = []
|
|
for raw in read_text_safe(env_file).splitlines():
|
|
line = raw.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
if "=" in line:
|
|
key, val = line.split("=", 1)
|
|
key = key.strip()
|
|
# strip inline comment after #
|
|
val = val.split("#", 1)[0].strip()
|
|
val = val.strip().strip('"').strip("'")
|
|
out.append({"name": key, "default": val})
|
|
return out
|
|
# -------------------------
|
|
# Build Portainer template object
|
|
# -------------------------
|
|
def generate_template_object(dir_path: Path, branch: str, nologo: bool) -> Dict:
|
|
name = dir_path.name
|
|
readme_path = dir_path / "README.md"
|
|
env_vars = parse_env_vars(dir_path)
|
|
obj = {
|
|
"type": 3,
|
|
"title": name,
|
|
"name": name,
|
|
"note": "Auto-generated template from repository",
|
|
"categories": ["Auto"],
|
|
"platform": "linux",
|
|
"logo": find_logo_url(dir_path, nologo=nologo, branch=branch),
|
|
"description": extract_overview(readme_path, name),
|
|
# Git repository for Compose stack
|
|
"repository": {
|
|
"url": f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}",
|
|
"stackfile": f"{name}/docker-compose.yml"
|
|
}
|
|
}
|
|
# env
|
|
env_entries = []
|
|
for v in env_vars:
|
|
if not v.get("name"):
|
|
continue
|
|
env_entries.append({"name": v["name"], "label": v["name"], "default": v.get("default","")})
|
|
if env_entries:
|
|
obj["env"] = env_entries
|
|
return obj
|
|
# -------------------------
|
|
# Main
|
|
# -------------------------
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Generate Portainer templates.json from compose cookbooks")
|
|
parser.add_argument("--nologo", action="store_true", help="Skip HEAD check for logos (faster)")
|
|
parser.add_argument("--branch", default=DEFAULT_BRANCH, help="Git branch to use for logo lookup (default: master). Templates use repo default branch.")
|
|
parser.add_argument("--output", default=str(DEFAULT_OUTPUT), help="Output path for templates.json")
|
|
parser.add_argument("--no-skip-volpath", action="store_true", help="Don't skip projects containing ${VOL_PATH")
|
|
args = parser.parse_args()
|
|
out_path = Path(args.output).resolve()
|
|
branch = args.branch
|
|
nologo = args.nologo
|
|
skip_volpath = not args.no_skip_volpath
|
|
templates = []
|
|
skipped = []
|
|
errored = []
|
|
for item in sorted(REPO_ROOT.iterdir()):
|
|
if not item.is_dir():
|
|
continue
|
|
if item.name.startswith("_"):
|
|
continue
|
|
if item.name.endswith(("_dev", "_notes")):
|
|
continue
|
|
compose_path = item / "docker-compose.yml"
|
|
if not compose_path.exists():
|
|
continue
|
|
text = read_text_safe(compose_path)
|
|
if skip_volpath and SKIP_VOLPATH_PATTERN in text:
|
|
skipped.append((item.name, "uses ${VOL_PATH}"))
|
|
continue
|
|
try:
|
|
tpl = generate_template_object(item, branch=branch, nologo=nologo)
|
|
templates.append(tpl)
|
|
except Exception as e:
|
|
errored.append((item.name, str(e)))
|
|
output = {"version": "2", "templates": templates}
|
|
# write atomic
|
|
write_atomic(out_path, json.dumps(output, indent=2))
|
|
print(f"Generated {out_path} with {len(templates)} templates.")
|
|
if skipped:
|
|
print("Skipped projects:")
|
|
for n, reason in skipped:
|
|
print(f" - {n}: {reason}")
|
|
if errored:
|
|
print("Errored projects:")
|
|
for n, err in errored:
|
|
print(f" - {n}: {err}")
|
|
if __name__ == "__main__":
|
|
main() |