Преглед изворни кода

new functions and template frontmatter settings

xcad пре 5 месеци
родитељ
комит
d0c4e848e9
48 измењених фајлова са 742 додато и 191 уклоњено
  1. 0 0
      add_frontmatter.py
  2. 7 3
      cli/core/models.py
  3. 183 32
      cli/core/prompt.py
  4. 43 0
      cli/core/variables.py
  5. 115 23
      cli/modules/compose/commands.py
  6. 9 10
      cli/modules/compose/variables.py
  7. 36 14
      library/compose/alloy/compose.yaml
  8. 10 0
      library/compose/ansiblesemaphore/compose.yaml
  9. 11 0
      library/compose/authentik/compose.yaml
  10. 10 0
      library/compose/bind9/compose.yaml
  11. 10 0
      library/compose/cadvisor/compose.yaml
  12. 10 0
      library/compose/checkmk/compose.yaml
  13. 10 0
      library/compose/clamav/compose.yaml
  14. 10 0
      library/compose/dockge/compose.yaml
  15. 0 16
      library/compose/duplicati/compose.yaml
  16. 0 1
      library/compose/factory/README.md
  17. 0 23
      library/compose/factory/runner-pool/compose.yaml
  18. 11 0
      library/compose/gitea/compose.yaml
  19. 11 0
      library/compose/gitlab-runner/compose.yaml
  20. 11 0
      library/compose/gitlab/compose.yaml
  21. 10 0
      library/compose/grafana/compose.yaml
  22. 11 0
      library/compose/heimdall/compose.yaml
  23. 11 0
      library/compose/homeassistant/compose.yaml
  24. 10 0
      library/compose/homepage/compose.yaml
  25. 10 0
      library/compose/homer/compose.yaml
  26. 10 0
      library/compose/influxdb/compose.yaml
  27. 10 0
      library/compose/loki/compose.yaml
  28. 10 0
      library/compose/mariadb/compose.yaml
  29. 10 0
      library/compose/nextcloud/compose.yaml
  30. 10 0
      library/compose/nginx/compose.yaml
  31. 10 0
      library/compose/nginxproxymanager/compose.yaml
  32. 10 0
      library/compose/nodeexporter/compose.yaml
  33. 0 14
      library/compose/nvidiadgcm/compose.yaml
  34. 0 16
      library/compose/nvidiasmi/compose.yaml
  35. 10 0
      library/compose/openwebui/compose.yaml
  36. 10 0
      library/compose/passbolt/compose.yaml
  37. 10 0
      library/compose/pihole/compose.yaml
  38. 10 0
      library/compose/portainer/compose.yaml
  39. 10 0
      library/compose/postgres/compose.yaml
  40. 10 0
      library/compose/prometheus/compose.yaml
  41. 10 0
      library/compose/promtail/compose.yaml
  42. 0 36
      library/compose/swag/compose.yaml
  43. 10 0
      library/compose/teleport/compose.yaml
  44. 10 0
      library/compose/traefik/compose.yaml
  45. 10 0
      library/compose/twingate_connector/compose.yaml
  46. 10 0
      library/compose/uptimekuma/compose.yaml
  47. 10 0
      library/compose/wazuh/compose.yaml
  48. 3 3
      library/compose/whoami/compose.yaml

+ 0 - 0
add_frontmatter.py


+ 7 - 3
cli/core/models.py

@@ -17,9 +17,11 @@ class Boilerplate:
         # Extract frontmatter fields with defaults
         self.name = frontmatter_data.get('name', file_path.stem)
         self.description = frontmatter_data.get('description', 'No description available')
-        self.author = frontmatter_data.get('author', 'Unknown')
-        self.date = frontmatter_data.get('date', 'Unknown')
-        self.module = frontmatter_data.get('module', 'Unknown')
+        self.author = frontmatter_data.get('author', '')
+        self.date = frontmatter_data.get('date', '')
+        self.version = frontmatter_data.get('version', '')
+        self.module = frontmatter_data.get('module', '')
+        self.tags = frontmatter_data.get('tags', [])
         self.files = frontmatter_data.get('files', [])
         
         # Additional computed properties
@@ -33,7 +35,9 @@ class Boilerplate:
             'description': self.description,
             'author': self.author,
             'date': self.date,
+            'version': self.version,
             'module': self.module,
+            'tags': self.tags,
             'files': self.files,
             'path': str(self.relative_path),
             'size': f"{self.size:,} bytes"

+ 183 - 32
cli/core/prompt.py

@@ -9,10 +9,13 @@ class PromptHandler:
         self.variable_sets = variable_sets
 
     @staticmethod
-    def ask_bool(prompt_text: str, default: bool = False) -> bool:
+    def ask_bool(prompt_text: str, default: bool = False, description: Optional[str] = None) -> bool:
         """Ask a yes/no question, render default in cyan when in a TTY, and
         fall back to typer.confirm when not attached to a TTY.
         """
+        if description and description.strip():
+            typer.secho(description, fg=typer.colors.BRIGHT_BLACK)
+            
         if not (sys.stdin.isatty() and sys.stdout.isatty()):
             return typer.confirm(prompt_text, default=default)
 
@@ -29,11 +32,15 @@ class PromptHandler:
         return r[0] in ("y", "1", "t")
 
     @staticmethod
-    def ask_int(prompt_text: str, default: Optional[int] = None) -> int:
+    def ask_int(prompt_text: str, default: Optional[int] = None, description: Optional[str] = None) -> int:
+        if description and description.strip():
+            typer.secho(description, fg=typer.colors.BRIGHT_BLACK)
         return IntPrompt.ask(prompt_text, default=default, show_default=True)
 
     @staticmethod
-    def ask_str(prompt_text: str, default: Optional[str] = None, show_default: bool = True) -> str:
+    def ask_str(prompt_text: str, default: Optional[str] = None, show_default: bool = True, description: Optional[str] = None) -> str:
+        if description and description.strip():
+            typer.secho(description, fg=typer.colors.BRIGHT_BLACK)
         return Prompt.ask(prompt_text, default=default, show_default=show_default)
 
     def collect_values(self, used_vars: Set[str], template_defaults: Dict[str, Any] = None, used_subscripts: Dict[str, Set[str]] = None) -> Dict[str, Any]:
@@ -72,11 +79,26 @@ class PromptHandler:
             typer.secho(f"\n{set_name.title()} Settings", fg=typer.colors.BLUE, bold=True)
 
             def _print_defaults_for_set(vars_list):
-                # Print each variable and its default value (field name: grey, value: cyan)
+                # Collect variables that have an effective default to print.
+                printable = []
                 for v in vars_list:
                     meta_info = self._declared[v][1]
                     display_name = meta_info.get("display_name", v.replace("_", " ").title())
                     default = self._get_effective_default(v, template_defaults, values)
+                    # Skip variables that have no effective default (they must be provided by the user)
+                    if default is None:
+                        continue
+                    printable.append((v, display_name, default))
+
+                # If there are no defaults to show, don't print a header or blank line.
+                if not printable:
+                    return
+
+                # Print a blank line and a consistent header for defaults so it matches
+                # the 'Required ... Variables' section formatting.
+                typer.secho("\nDefault %s Variables" % set_name.title(), fg=typer.colors.GREEN, bold=True)
+
+                for v, display_name, default in printable:
                     # If variable is accessed with subscripts, show '(multiple)'
                     if used_subscripts and v in used_subscripts and used_subscripts[v]:
                         typer.secho(f"{display_name}: ", fg=typer.colors.BRIGHT_BLACK, nl=False)
@@ -117,29 +139,150 @@ class PromptHandler:
 
             # If we didn't ask prompt_enable, ask the customize prompt directly
             if enable_set is None:
-                # In this mode we treat the customize prompt as the enable decision.
-                change_set = self.ask_bool(set_customize_prompt, default=False)
-                values[set_name] = change_set
-                if set_name in vars_in_set:
-                    vars_in_set = [v for v in vars_in_set if v != set_name]
-                if not change_set:
-                    # Use defaults for this set
-                    for var in vars_in_set:
+                # Check for undefined variables first, before asking if they want to enable
+                undefined_vars_in_set = []
+                for var in vars_in_set:
+                    effective_default = self._get_effective_default(var, template_defaults, values)
+                    if effective_default is None:
+                        undefined_vars_in_set.append(var)
+                
+                # If there are undefined variables, we must enable this set
+                if undefined_vars_in_set:
+                    typer.secho(f"\n{set_name.title()} Settings", fg=typer.colors.BLUE, bold=True)
+                    typer.secho(f"Required {set_name.title()} Variables", fg=typer.colors.YELLOW, bold=True)
+                    for var in undefined_vars_in_set:
                         meta_info = self._declared[var][1]
-                        default = self._get_effective_default(var, template_defaults, values)
-                        values[var] = default
-                    continue
+                        display_name = meta_info.get("display_name", var.replace("_", " ").title())
+                        vtype = meta_info.get("type", "str")
+                        prompt = meta_info.get("prompt", f"Enter {display_name}")
+                        description = meta_info.get("description")
+                        
+                        # Handle subscripted variables
+                        subs = used_subscripts.get(var, set()) if used_subscripts else set()
+                        if subs:
+                            result_map = {}
+                            for k in subs:
+                                # Required sub-key: enforce non-empty
+                                kval = Prompt.ask(f"Value for {display_name}['{k}']:", default="", show_default=False)
+                                if not (sys.stdin.isatty() and sys.stdout.isatty()):
+                                    # Non-interactive: empty value is an error
+                                    if kval is None or str(kval).strip() == "":
+                                        typer.secho(f"[red]Required value for {display_name}['{k}'] cannot be blank in non-interactive mode.[/red]")
+                                        raise typer.Exit(code=1)
+                                else:
+                                    # Interactive: re-prompt until non-empty
+                                    while kval is None or str(kval).strip() == "":
+                                        typer.secho("Value cannot be blank. Please enter a value.", fg=typer.colors.YELLOW)
+                                        kval = Prompt.ask(f"Value for {display_name}['{k}']:", default="", show_default=False)
+                                result_map[k] = self._guess_and_cast(kval)
+                            values[var] = result_map
+                            continue
+
+                        if vtype == "bool":
+                            val = self.ask_bool(prompt, default=False, description=description)
+                        elif vtype == "int":
+                            val = self.ask_int(prompt, default=None, description=description)
+                        else:
+                            val = self.ask_str(prompt, default=None, show_default=False, description=description)
+                        
+                        values[var] = self._cast_value_from_input(val, vtype)
+                    
+                    # Since we prompted for required variables, enable the set
+                    values[set_name] = True
+                    if set_name in vars_in_set:
+                        vars_in_set = [v for v in vars_in_set if v != set_name]
+                    
+                    # Print defaults and ask if they want to change others
+                    _print_defaults_for_set(vars_in_set)
+                    change_set = self.ask_bool(set_customize_prompt, default=False)
+                    if not change_set:
+                        # Use defaults for remaining variables
+                        for var in vars_in_set:
+                            if var not in values:  # Don't override variables we already prompted for
+                                meta_info = self._declared[var][1]
+                                default = self._get_effective_default(var, template_defaults, values)
+                                values[var] = default
+                        continue
+                else:
+                    # No undefined variables, ask the customize prompt as normal
+                    change_set = self.ask_bool(set_customize_prompt, default=False)
+                    values[set_name] = change_set
+                    if set_name in vars_in_set:
+                        vars_in_set = [v for v in vars_in_set if v != set_name]
+                    if not change_set:
+                        # Use defaults for this set
+                        for var in vars_in_set:
+                            if var not in values:  # Don't override variables that might have been set
+                                meta_info = self._declared[var][1]
+                                default = self._get_effective_default(var, template_defaults, values)
+                                values[var] = default
+                        continue
 
             # If we had an enable_set (True/False) and it is False, skip customizing
             if enable_set is not None and not enable_set:
                 for var in vars_in_set:
-                    meta_info = self._declared[var][1]
-                    default = self._get_effective_default(var, template_defaults, values)
-                    values[var] = default
+                    if var not in values:  # Don't override variables that might have been set
+                        meta_info = self._declared[var][1]
+                        default = self._get_effective_default(var, template_defaults, values)
+                        values[var] = default
                 continue
 
-            # At this point the set is enabled. Print defaults now (only after
-            # enabling) so the user sees current values before customizing.
+            # At this point the set is enabled. Check for undefined variables first.
+            undefined_vars_in_set = []
+            for var in vars_in_set:
+                effective_default = self._get_effective_default(var, template_defaults, values)
+                if effective_default is None:
+                    undefined_vars_in_set.append(var)
+            
+            # Prompt for undefined variables in this set
+            if undefined_vars_in_set:
+                typer.secho(f"\nRequired {set_name.title()} Variables", fg=typer.colors.YELLOW, bold=True)
+                for var in undefined_vars_in_set:
+                    meta_info = self._declared[var][1]
+                    display_name = meta_info.get("display_name", var.replace("_", " ").title())
+                    vtype = meta_info.get("type", "str")
+                    prompt = meta_info.get("prompt", f"Enter {display_name}")
+                    description = meta_info.get("description")
+                    
+                    # Handle subscripted variables
+                    subs = used_subscripts.get(var, set()) if used_subscripts else set()
+                    if subs:
+                        result_map = {}
+                        for k in subs:
+                            # Required sub-key: enforce non-empty
+                            kval = Prompt.ask(f"Value for {display_name}['{k}']:", default="", show_default=False)
+                            if not (sys.stdin.isatty() and sys.stdout.isatty()):
+                                if kval is None or str(kval).strip() == "":
+                                    typer.secho(f"[red]Required value for {display_name}['{k}'] cannot be blank in non-interactive mode.[/red]")
+                                    raise typer.Exit(code=1)
+                            else:
+                                while kval is None or str(kval).strip() == "":
+                                    typer.secho("Value cannot be blank. Please enter a value.", fg=typer.colors.YELLOW)
+                                    kval = Prompt.ask(f"Value for {display_name}['{k}']:", default="", show_default=False)
+                            result_map[k] = self._guess_and_cast(kval)
+                        values[var] = result_map
+                        continue
+
+                    if vtype == "bool":
+                        val = self.ask_bool(prompt, default=False, description=description)
+                    elif vtype == "int":
+                        val = self.ask_int(prompt, default=None, description=description)
+                    else:
+                        val = self.ask_str(prompt, default=None, show_default=False, description=description)
+                        # Enforce non-empty for required scalar variables
+                        if not (sys.stdin.isatty() and sys.stdout.isatty()):
+                            if val is None or str(val).strip() == "":
+                                typer.secho(f"[red]Required value for {display_name} cannot be blank in non-interactive mode.[/red]")
+                                raise typer.Exit(code=1)
+                        else:
+                            while val is None or str(val).strip() == "":
+                                typer.secho("Value cannot be blank. Please enter a value.", fg=typer.colors.YELLOW)
+                                val = self.ask_str(prompt, default=None, show_default=False, description=description)
+
+                    values[var] = self._cast_value_from_input(val, vtype)
+
+            # Print defaults now (only after enabling and prompting for required vars)
+            # so the user sees current values before customizing.
             _print_defaults_for_set(vars_in_set)
 
             # If we have asked prompt_enable earlier (and the set is enabled),
@@ -149,17 +292,23 @@ class PromptHandler:
                 change_set = self.ask_bool(set_customize_prompt, default=False)
                 if not change_set:
                     for var in vars_in_set:
-                        meta_info = self._declared[var][1]
-                        default = self._get_effective_default(var, template_defaults, values)
-                        values[var] = default
+                        if var not in values:  # Don't override variables that might have been set
+                            meta_info = self._declared[var][1]
+                            default = self._get_effective_default(var, template_defaults, values)
+                            values[var] = default
                     continue
 
             # Prompt for each variable in the set
             for var in vars_in_set:
+                # Skip variables that have already been prompted for
+                if var in values:
+                    continue
+                    
                 meta_info = self._declared[var][1]
                 display_name = meta_info.get("display_name", var.replace("_", " ").title())
                 vtype = meta_info.get("type", "str")
                 prompt = meta_info.get("prompt", f"Enter {display_name}")
+                description = meta_info.get("description")
                 default = self._get_effective_default(var, template_defaults, values)
 
                 # Build prompt text and rely on show_default to display the default value
@@ -193,7 +342,7 @@ class PromptHandler:
                         bool_default = default.lower() in ("true", "1", "yes")
                     elif isinstance(default, int):
                         bool_default = default != 0
-                    val = self.ask_bool(prompt_text, default=bool_default)
+                    val = self.ask_bool(prompt_text, default=bool_default, description=description)
                 elif vtype == "int":
                     # Use IntPrompt to validate and parse integers; show default if present
                     int_default = None
@@ -201,11 +350,11 @@ class PromptHandler:
                         int_default = default
                     elif isinstance(default, str) and default.isdigit():
                         int_default = int(default)
-                    val = IntPrompt.ask(prompt_text, default=int_default, show_default=True)
+                    val = self.ask_int(prompt_text, default=int_default, description=description)
                 else:
                     # Use Prompt for string input and show default
                     str_default = str(default) if default is not None else None
-                    val = Prompt.ask(prompt_text, default=str_default, show_default=True)
+                    val = self.ask_str(prompt_text, default=str_default, show_default=True, description=description)
 
                 # Handle collection types: arrays and maps
                 if vtype in ("array", "list"):
@@ -284,6 +433,7 @@ class PromptHandler:
         display_name = meta_info.get("display_name", var_name.replace("_", " ").title())
         vtype = meta_info.get("type", "str")
         prompt = meta_info.get("prompt", f"Enter {display_name}")
+        description = meta_info.get("description")
         if vtype == "bool":
             bool_default = False
             if isinstance(default_val, bool):
@@ -292,16 +442,16 @@ class PromptHandler:
                 bool_default = default_val.lower() in ("true", "1", "yes")
             elif isinstance(default_val, int):
                 bool_default = default_val != 0
-            return self.ask_bool(prompt, default=bool_default)
+            return self.ask_bool(prompt, default=bool_default, description=description)
         if vtype == "int":
             int_default = None
             if isinstance(default_val, int):
                 int_default = default_val
             elif isinstance(default_val, str) and default_val.isdigit():
                 int_default = int(default_val)
-            return self.ask_int(prompt, default=int_default)
+            return self.ask_int(prompt, default=int_default, description=description)
         str_default = str(default_val) if default_val is not None else None
-        return self.ask_str(prompt, default=str_default, show_default=True)
+        return self.ask_str(prompt, default=str_default, show_default=True, description=description)
 
     def prompt_array(self, var_name: str, meta_info: Dict[str, Any], default_val: Any) -> Any:
         display_name = meta_info.get("display_name", var_name.replace("_", " ").title())
@@ -395,6 +545,7 @@ class PromptHandler:
         display_name = meta_info.get("display_name", var_name.replace("_", " ").title())
         vtype = meta_info.get("type", "str")
         prompt = meta_info.get("prompt", f"Enter {display_name}")
+        description = meta_info.get("description")
         if vtype == "bool":
             bool_default = False
             if isinstance(default_val, bool):
@@ -403,16 +554,16 @@ class PromptHandler:
                 bool_default = default_val.lower() in ("true", "1", "yes")
             elif isinstance(default_val, int):
                 bool_default = default_val != 0
-            return self.ask_bool(prompt, default=bool_default)
+            return self.ask_bool(prompt, default=bool_default, description=description)
         if vtype == "int":
             int_default = None
             if isinstance(default_val, int):
                 int_default = default_val
             elif isinstance(default_val, str) and default_val.isdigit():
                 int_default = int(default_val)
-            return self.ask_int(prompt, default=int_default)
+            return self.ask_int(prompt, default=int_default, description=description)
         str_default = str(default_val) if default_val is not None else None
-        return self.ask_str(prompt, default=str_default, show_default=True)
+        return self.ask_str(prompt, default=str_default, show_default=True, description=description)
 
     def prompt_array(self, var_name: str, meta_info: Dict[str, Any], default_val: Any) -> Any:
         display_name = meta_info.get("display_name", var_name.replace("_", " ").title())

+ 43 - 0
cli/core/variables.py

@@ -142,6 +142,49 @@ class BaseVariables:
 
         return defaults
 
+    def extract_variable_meta_overrides(self, template_content: str) -> Dict[str, Dict[str, Any]]:
+        """Extract variable metadata overrides from a Jinja2 block.
+
+        Supports a block like:
+
+        {% variables %}
+        container_hostname:
+          description: "..."
+        {% endvariables %}
+
+        The contents are parsed as YAML and returned as a dict mapping
+        variable name -> metadata overrides.
+        """
+        import re
+        try:
+            m = re.search(r"\{%\s*variables\s*%\}(.+?)\{%\s*endvariables\s*%\}", template_content, flags=re.S)
+            if not m:
+                return {}
+            yaml_block = m.group(1).strip()
+            try:
+                import yaml
+            except Exception:
+                return {}
+            try:
+                data = yaml.safe_load(yaml_block) or {}
+                if isinstance(data, dict):
+                    # Ensure values are dicts
+                    cleaned: Dict[str, Dict[str, Any]] = {}
+                    for k, v in data.items():
+                        if v is None:
+                            cleaned[k] = {}
+                        elif isinstance(v, dict):
+                            cleaned[k] = v
+                        else:
+                            # If a scalar was provided, interpret as description
+                            cleaned[k] = {"description": v}
+                    return cleaned
+            except Exception:
+                return {}
+        except Exception:
+            return {}
+        return {}
+
     def determine_variable_sets(self, template_content: str) -> Tuple[List[str], Set[str]]:
         """Return a list of variable set names that contain any used variables.
 

+ 115 - 23
cli/modules/compose/commands.py

@@ -3,10 +3,12 @@ Compose module commands and functionality.
 Manage Compose configurations and services and template operations.
 """
 
+import re
 import typer
 from pathlib import Path
 from rich.console import Console
 from rich.table import Table
+from rich.syntax import Syntax
 from typing import List, Optional, Set, Dict, Any
 
 from ...core.command import BaseModule
@@ -78,7 +80,8 @@ class ComposeModule(BaseModule):
         def show(name: str, raw: bool = typer.Option(False, "--raw", help="Output only the raw boilerplate content")):
             """Show details about a compose boilerplate by name."""
             bps = find_boilerplates(self.library_path, self.compose_filenames)
-            bp = next((b for b in bps if b.name.lower() == name.lower()), None)
+            # Match by directory name (parent folder of the compose file) instead of frontmatter 'name'
+            bp = next((b for b in bps if b.file_path.parent.name.lower() == name.lower()), None)
             if not bp:
                 self.console.print(f"[red]Boilerplate '{name}' not found.[/red]")
                 return
@@ -86,24 +89,62 @@ class ComposeModule(BaseModule):
                 # Output only the raw boilerplate content
                 print(bp.content)
                 return
-            # Print frontmatter info in a clever way
-            table = Table(title=f"🐳 Boilerplate: {bp.name}", title_style="bold blue")
-            table.add_column("Field", style="cyan", no_wrap=True)
-            table.add_column("Value", style="green")
+            # Print frontmatter info in a clean, readable format
+            from rich.text import Text
+            from rich.console import Group
+            
             info = bp.to_dict()
-            for key, value in info.items():
-                if isinstance(value, List):
-                    value = ", ".join(str(v) for v in value)
-                table.add_row(key.title(), str(value))
-            self.console.print(table)
+            
+            # Create a clean header
+            header = Text()
+            header.append("🐳 Boilerplate: ", style="bold")
+            header.append(f"{info['name']}", style="bold blue")
+            header.append(f" ({info['version']})", style="magenta")
+            header.append("\n", style="bold")
+            header.append(f"{info['description']}", style="dim white")
+            
+            # Create metadata section with clean formatting
+            metadata = Text()
+            metadata.append("\nDetails:\n", style="bold cyan")
+            metadata.append("─" * 40 + "\n", style="dim cyan")
+            
+            # Format each field with consistent styling
+            fields = [
+                ("Tags", ", ".join(info['tags']), "cyan"),
+                ("Author", info['author'], "dim white"), 
+                ("Date", info['date'], "dim white"),
+                ("Size", info['size'], "dim white"),
+                ("Path", info['path'], "dim white")
+            ]
+            
+            for label, value, color in fields:
+                metadata.append(f"{label}: ")
+                metadata.append(f"{value}\n", style=color)
+            
+            # Handle files list if present
+            if info['files'] and len(info['files']) > 0:
+                metadata.append("  Files: ")
+                files_str = ", ".join(info['files'][:3])  # Show first 3
+                if len(info['files']) > 3:
+                    files_str += f" ... and {len(info['files']) - 3} more"
+                metadata.append(f"{files_str}\n", style="green")
+            
+            # Display everything as a group
+            display_group = Group(header, metadata)
+            self.console.print(display_group)
+
 
             # Show the content of the boilerplate file in a cleaner form
             from rich.panel import Panel
             from rich.syntax import Syntax
-            self.console.print()  # Add spacing
 
-            # Use syntax highlighting for YAML files
-            syntax = Syntax(bp.content, "yaml", theme="monokai", line_numbers=True, word_wrap=True)
+            # Detect if content contains Jinja2 templating
+            has_jinja = bool(re.search(r'\{\{.*\}\}|\{\%.*\%\}|\{\#.*\#\}', bp.content))
+            
+            # Use appropriate lexer based on content
+            # Use yaml+jinja for combined YAML and Jinja2 highlighting when Jinja2 is present
+            lexer = "yaml+jinja" if has_jinja else "yaml"
+            syntax = Syntax(bp.content, lexer, theme="monokai", line_numbers=True, word_wrap=True)
             panel = Panel(syntax, title=f"{bp.file_path.name}", border_style="blue", padding=(1,2))
             self.console.print(panel)
 
@@ -120,13 +161,20 @@ class ComposeModule(BaseModule):
         ):
             """Render a compose boilerplate interactively and write output to --out."""
             bps = find_boilerplates(self.library_path, self.compose_filenames)
-            bp = next((b for b in bps if b.name.lower() == name.lower()), None)
+            # Match by directory name (parent folder of the compose file) instead of frontmatter 'name'
+            bp = next((b for b in bps if b.file_path.parent.name.lower() == name.lower()), None)
             if not bp:
                 self.console.print(f"[red]Boilerplate '{name}' not found.[/red]")
                 raise typer.Exit(code=1)
 
             cv = ComposeVariables()
-            matched_sets, used_vars = cv.determine_variable_sets(bp.content)
+            # Remove any in-template `{% variables %} ... {% endvariables %}` block
+            # before asking Jinja2 to parse/render the template. This block is
+            # used only to provide metadata overrides and is not valid Jinja2
+            # syntax for the default parser (unknown tag -> TemplateSyntaxError).
+            import re
+            cleaned_content = re.sub(r"\{%\s*variables\s*%\}(.+?)\{%\s*endvariables\s*%\}\n?", "", bp.content, flags=re.S)
+            matched_sets, used_vars = cv.determine_variable_sets(cleaned_content)
 
             # If there are no detected variable sets but there are used vars, we still
             # need to prompt for the used variables. Lazy-import jinja2 only when
@@ -140,7 +188,36 @@ class ComposeModule(BaseModule):
                     typer.secho("Jinja2 is required to render templates. Install it and retry.", fg=typer.colors.RED)
                     raise typer.Exit(code=2)
 
-                template_defaults = cv.extract_template_defaults(bp.content)
+                # Use the cleaned content for defaults and rendering, but extract
+                # overrides from the original content (which may contain the
+                # variables block).
+                template_defaults = cv.extract_template_defaults(cleaned_content)
+
+                # Validate Jinja2 template syntax before proceeding. Parsing the
+                # template will surface syntax errors (unclosed blocks, invalid
+                # tags, etc.) early and allow us to abort with a helpful message.
+                try:
+                    env_for_validation = jinja2.Environment(loader=jinja2.BaseLoader())
+                    env_for_validation.parse(cleaned_content)
+                except jinja2.exceptions.TemplateSyntaxError as e:
+                    # Show file path (if available) and error details, then exit.
+                    self.console.print(f"[red]Template syntax error in '{bp.file_path}': {e.message} (line {e.lineno})[/red]")
+                    raise typer.Exit(code=2)
+                except Exception as e:
+                    # Generic parse failure
+                    self.console.print(f"[red]Failed to parse template '{bp.file_path}': {e}[/red]")
+                    raise typer.Exit(code=2)
+                # Extract variable metadata overrides from a {% variables %} block
+                try:
+                    meta_overrides = cv.extract_variable_meta_overrides(bp.content)
+                    # Merge overrides into declared metadata so PromptHandler will pick them up
+                    for var_name, overrides in meta_overrides.items():
+                        if var_name in cv._declared and isinstance(overrides, dict):
+                            existing = cv._declared[var_name][1]
+                            # shallow merge
+                            existing.update(overrides)
+                except Exception:
+                    meta_overrides = {}
                 used_subscripts = cv.find_used_subscript_keys(bp.content)
                 
                 # Load values from file if specified
@@ -203,18 +280,33 @@ class ComposeModule(BaseModule):
                 # Enable Jinja2 whitespace control so that block tags like
                 # {% if %} don't leave an extra newline in the rendered result.
                 env = jinja2.Environment(loader=jinja2.BaseLoader(), trim_blocks=True, lstrip_blocks=True)
-                template = env.from_string(bp.content)
-                rendered = template.render(**values_dict)
+                try:
+                    template = env.from_string(cleaned_content)
+                except jinja2.exceptions.TemplateSyntaxError as e:
+                    self.console.print(f"[red]Template syntax error in '{bp.file_path}': {e.message} (line {e.lineno})[/red]")
+                    raise typer.Exit(code=2)
+                except Exception as e:
+                    self.console.print(f"[red]Failed to compile template '{bp.file_path}': {e}[/red]")
+                    raise typer.Exit(code=2)
+
+                try:
+                    rendered = template.render(**values_dict)
+                except jinja2.exceptions.TemplateError as e:
+                    # Catch runtime/template errors (undefined variables, etc.)
+                    self.console.print(f"[red]Template rendering error for '{bp.file_path}': {e}[/red]")
+                    raise typer.Exit(code=2)
+                except Exception as e:
+                    self.console.print(f"[red]Unexpected error while rendering '{bp.file_path}': {e}[/red]")
+                    raise typer.Exit(code=2)
 
             # If --out not provided, print to console; else write to file
 
             if out is None:
-                from rich.panel import Panel
-                from rich.syntax import Syntax
-
+                # Print a subtle rule and a small header, then the highlighted YAML
+                self.console.print(f"\n\nGenerated Boilerplate for [bold cyan]{bp.name}[/bold cyan]\n")
                 syntax = Syntax(rendered, "yaml", theme="monokai", line_numbers=False, word_wrap=True)
-                panel = Panel(syntax, title=f"{bp.name}", border_style="green", padding=(1,2))
-                self.console.print(panel)
+                self.console.print(syntax)
+
             else:
                 # Ensure parent directory exists
                 out_parent = out.parent

+ 9 - 10
cli/modules/compose/variables.py

@@ -16,13 +16,11 @@ class ComposeVariables(BaseVariables):
             "prompt": "Do you want to change the general settings?",
             "variables": {
                     "service_name": {"display_name": "Service name", "default": None, "type": "str", "prompt": "Enter service name"},
-                    "service_port": {
-                        "display_name": "Service port",
-                        "type": "int",
-                        "prompt": "Enter service port(s)",
-                    },
-                    "container_name": {"display_name": "Container name", "default": "", "type": "str", "prompt": "Enter container name"},
+                    "service_port": {"display_name": "Service port", "default": None, "type": "int", "prompt": "Enter service port(s)", "description": "Port number(s) the service will expose (has to be a single port)"},
+                    "container_name": {"display_name": "Container name", "default": None, "type": "str", "prompt": "Enter container name"},
+                    "container_hostname": {"display_name": "Container hostname", "default": None, "type": "str", "prompt": "Enter container hostname", "description": "Hostname that will be set inside the container"},
                     "docker_network": {"display_name": "Docker network", "default": "bridge", "type": "str", "prompt": "Enter Docker network name"},
+                    "restart_policy": {"display_name": "Restart policy", "default": "unless-stopped", "type": "str", "prompt": "Enter restart policy"},
             },
         },
         "swarm": {
@@ -37,10 +35,11 @@ class ComposeVariables(BaseVariables):
             "prompt": "Do you want to change the Traefik labels?",
             "variables": {
                 "traefik_enable": {"display_name": "Enable Traefik", "default": True, "type": "bool", "prompt": "Enable Traefik routing for this service?"},
-                "traefik_host": {"display_name": "Routing Rule Host", "default": "", "type": "str", "prompt": "Enter hostname for the routing rule (e.g., example.com))"},
-                "traefik_tls": {"display_name": "Enable TLS", "default": True, "type": "bool", "prompt": "Enable TLS for this router?"},
-                "traefik_certresolver": {"display_name": "Certificate resolver", "default": "cloudflare", "type": "str", "prompt": "Enter certificate resolver name"},
-                "traefik_middleware": {"display_name": "Middlewares", "default": "", "type": "str", "prompt": "Enter middlewares (comma-separated, leave empty for none)"},
+                "traefik_host": {"display_name": "Routing Rule Host", "default": None, "type": "str", "prompt": "Enter hostname for the routing rule (e.g., example.com))", "description": "Domain name that Traefik will use to route traffic to this service"},
+                "traefik_tls": {"display_name": "Enable TLS", "default": False, "type": "bool", "prompt": "Enable TLS for this router?", "description": "Whether to enable HTTPS/TLS encryption for this route"},
+                "traefik_certresolver": {"display_name": "Certificate resolver", "type": "str", "prompt": "Enter certificate resolver name", "description": "Name of the certificate resolver to use for obtaining SSL certificates"},
+                "traefik_middleware": {"display_name": "Middlewares", "default": None, "type": "str", "prompt": "Enter middlewares (comma-separated, leave empty for none)", "description": "Comma-separated list of Traefik middlewares to apply to this route"},
+                "traefik_entrypoint": {"display_name": "EntryPoint", "default": "web", "type": "str", "prompt": "Enter entrypoint name", "description": "Name of the Traefik entrypoint to use for this router"},
             },
         },
     }

+ 36 - 14
library/compose/alloy/compose.yaml

@@ -1,9 +1,25 @@
 ---
+name: "Grafana Alloy"
+description: "A lightweight and flexible service mesh"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - "grafana"
+  - "alloy"
+  - "monitoring"
+  - "http"
+  - "traefik"
+---
+{% variables %}
+container_hostname:
+  description: "Sets the container's internal hostname (this will show up in the collected logs)"
+{% endvariables %}
 services:
-  alloy:
+  {{ service_name | default("alloy")}}:
     image: grafana/alloy:v1.10.2
-    container_name: alloy
-    hostname: your-server-name
+  container_name: {{ container_name | default("alloy") }}
+    hostname: {{ container_hostname }}
     command:
       - run
       - --server.http.listen-addr=0.0.0.0:12345
@@ -21,22 +37,28 @@ services:
       - /var/lib/docker/:/var/lib/docker/:ro
       - /run/udev/data:/run/udev/data:ro
     networks:
-      - frontend
+      - {{ docker_network | default("bridge")}}
+    {% if traefik %}
     labels:
-      - traefik.enable=true
-      - traefik.http.services.alloy.loadbalancer.server.port=12345
-      - traefik.http.services.alloy.loadbalancer.server.scheme=http
-      - traefik.http.routers.alloy.service=alloy
-      - traefik.http.routers.alloy.rule=Host(`alloy.home.arpa`)
-      - traefik.http.routers.alloy.entrypoints=websecure
-      - traefik.http.routers.alloy.tls=true
-      - traefik.http.routers.alloy.tls.certresolver=cloudflare
-    restart: unless-stopped
+      - traefik.enable={{ traefik_enable | default(true) }}
+      - traefik.http.services.{{ service_name }}.loadbalancer.server.port=12345
+      - traefik.http.services.{{ service_name }}.loadbalancer.server.scheme=http
+      - traefik.http.routers.{{ service_name }}.service={{ service_name }}
+      - traefik.http.routers.{{ service_name }}.rule=Host(`{{ traefik_host }}`)
+      {% if traefik_tls %}
+      - traefik.http.routers.{{ service_name }}.tls={{ traefik_tls | default(true) }}
+      - traefik.http.routers.{{ service_name }}.entrypoints={{ traefik_entrypoint | default("websecure") }}
+      - traefik.http.routers.{{ service_name }}.tls.certresolver={{ traefik_certresolver | default("cloudflare") }}
+      {% else %}
+      - traefik.http.routers.{{ service_name }}.entrypoints={{ traefik_entrypoint | default("websecure") }}
+      {% endif %}
+    {% endif %}
+    restart: {{ restart_policy | default("unless-stopped") }}
 
 volumes:
   alloy_data:
     driver: local
 
 networks:
-  frontend:
+  {{ docker_network | default("bridge")}}:
     external: true

+ 10 - 0
library/compose/ansiblesemaphore/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Ansible Semaphore"
+description: "A powerful and flexible automation tool"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - "ansible"
+  - "automation"
+  - "semaphore"
+---
 volumes:
   semaphore-mysql:
     driver: local

+ 11 - 0
library/compose/authentik/compose.yaml

@@ -1,4 +1,15 @@
 ---
+name: "Authentik"
+description: "An open-source identity and access management solution"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - "authentik"
+  - "identity"
+  - "access"
+  - "management"  
+---
 services:
   server:
     image: ghcr.io/goauthentik/server:2025.6.3

+ 10 - 0
library/compose/bind9/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "BIND9"
+description: "A powerful and flexible DNS server"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - "bind9"
+  - "dns"
+  - "server"
+---
 services:
   bind9:
     image: docker.io/ubuntu/bind9:9.20-24.10_edge

+ 10 - 0
library/compose/cadvisor/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "cAdvisor"
+description: "A tool for monitoring container performance"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - "cadvisor"
+  - "monitoring"
+  - "containers"
+---
 services:
   cadvisor:
     image: gcr.io/cadvisor/cadvisor:v0.52.1

+ 10 - 0
library/compose/checkmk/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Checkmk"
+description: "A powerful monitoring solution"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - "checkmk"
+  - "monitoring"
+  - "observability"
+---
 services:
   monitoring:
     image: checkmk/check-mk-raw:2.4.0-latest

+ 10 - 0
library/compose/clamav/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "ClamAV"
+description: "An open-source antivirus engine"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - "clamav"
+  - "antivirus"
+  - "security"
+---
 services:
   clamav:
     image: docker.io/clamav/clamav:1.4.3

+ 10 - 0
library/compose/dockge/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Dockge"
+description: "A Docker GUI for managing your containers"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - "dockge"
+  - "docker"
+  - "management"
+---
 services:
   dockge:
     container_name: dockge

+ 0 - 16
library/compose/duplicati/compose.yaml

@@ -1,16 +0,0 @@
----
-services:
-  duplicati:
-    image: lscr.io/linuxserver/duplicati:2.1.0
-    container_name: duplicati
-    environment:
-      - PUID=1000
-      - PGID=1000
-      - TZ=Europe/Berlin
-    volumes:
-      - /AmberPRO/duplicati/config:/config
-      - /Backups:/backups
-      - /:/source
-    ports:
-      - 8200:8200
-    restart: unless-stopped

+ 0 - 1
library/compose/factory/README.md

@@ -1 +0,0 @@
-

+ 0 - 23
library/compose/factory/runner-pool/compose.yaml

@@ -1,23 +0,0 @@
----
-services:
-  refactr-runner:
-    container_name: factory-runnerpool-prod-1
-    image: docker.io/refactr/runner-pool:v0.153.4
-    user: root
-    volumes:
-      - /run/docker.sock:/run/docker.sock
-      - ./config.json:/etc/runner-agent.json
-    # stdin_open: true
-    # tty: true
-    environment:
-      - ENVIRONMENT=eval
-      - LOG_LEVEL=debug
-      - RUNNER_MANAGER_ID=${RUNNER_MANAGER_ID}
-      - RUNNER_MANAGER_KEY=${RUNNER_MANAGER_KEY}
-      - CONFIG_PATH=/etc/runner-agent.json
-      - NEW_RELIC_ENABLED=false
-      - NEW_RELIC_APP_NAME=factory-runnerpool-prod-1
-      - RUNNER_LOCAL_DOCKER_IMAGE_REGISTRY=docker.io
-      - RUNNER_LOCAL_DOCKER_IMAGE_REPOSITORY=refactr/runner
-      - RUNNER_LOCAL_DOCKER_IMAGE_TAG=latest
-    restart: unless-stopped

+ 11 - 0
library/compose/gitea/compose.yaml

@@ -1,4 +1,15 @@
 ---
+name: "Gitea"
+description: "A self-hosted Git service"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+ - gitea
+ - git
+ - code
+ - repository
+---
 services:
   server:
     image: docker.io/gitea/gitea:1.24.5

+ 11 - 0
library/compose/gitlab-runner/compose.yaml

@@ -1,4 +1,15 @@
 ---
+name: "GitLab Runner"
+description: "A self-hosted CI/CD automation tool"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - gitlab-runner
+  - ci
+  - cd
+  - automation
+---
 services:
   gitlab-runner:
     image: docker.io/gitlab/gitlab-runner:alpine-v17.9.1

+ 11 - 0
library/compose/gitlab/compose.yaml

@@ -1,4 +1,15 @@
 ---
+name: "GitLab"
+description: "A self-hosted Git repository manager"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - gitlab
+  - git
+  - repository
+  - management
+---
 services:
   gitlab:
     image: docker.io/gitlab/gitlab-ce:18.3.1-ce.0

+ 10 - 0
library/compose/grafana/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Grafana"
+description: "An open-source platform for monitoring and observability"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - grafana
+  - monitoring
+  - observability
+---
 volumes:
   grafana-data:
     driver: local

+ 11 - 0
library/compose/heimdall/compose.yaml

@@ -1,4 +1,15 @@
 ---
+name: "Heimdall"
+description: "An open-source dashboard for your web applications"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - heimdall
+  - dashboard
+  - monitoring
+  - observability
+---
 services:
   heimdall:
     image: lscr.io/linuxserver/heimdall:2.7.4

+ 11 - 0
library/compose/homeassistant/compose.yaml

@@ -1,4 +1,15 @@
 ---
+name: "Home Assistant"
+description: "A self-hosted home automation platform"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - homeassistant
+  - automation
+  - monitoring
+  - observability
+---
 services:
   homeassistant:
     container_name: homeassistant

+ 10 - 0
library/compose/homepage/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Homepage"
+description: "A self-hosted homepage for your web applications"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - homepage
+  - web
+  - dashboard
+---
 services:
   homepage:
     image: ghcr.io/gethomepage/homepage:v1.4.6

+ 10 - 0
library/compose/homer/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Homer"
+description: "A simple homepage for your services"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - "homer"
+  - "http"
+  - "testing"
+---
 services:
   homer:
     image: docker.io/b4bz/homer:v25.08.1

+ 10 - 0
library/compose/influxdb/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "InfluxDB"
+description: "An open-source time series database"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - influxdb
+  - monitoring
+  - database
+---
 # (Optional) when using custom network
 # networks:
 #   yournetwork:

+ 10 - 0
library/compose/loki/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Loki"
+description: "An open-source log aggregation system"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - loki
+  - monitoring
+  - logging
+---
 services:
   loki:
     container_name: loki

+ 10 - 0
library/compose/mariadb/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "MariaDB"
+description: "An open-source relational database management system"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - mariadb
+  - database
+  - sql
+---
 # (Optional) when using custom network
 # networks:
 #   yournetwork:

+ 10 - 0
library/compose/nextcloud/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Nextcloud"
+description: "A self-hosted file sync and share platform"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - nextcloud
+  - web
+  - file-storage
+---
 volumes:
   nextcloud-data:
   nextcloud-db:

+ 10 - 0
library/compose/nginx/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Nginx"
+description: "An open-source web server"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - nginx
+  - web
+  - reverse-proxy
+---
 services:
   {{ service_name | default('nginx') }}:
     image: docker.io/library/nginx:1.28.0-alpine

+ 10 - 0
library/compose/nginxproxymanager/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Nginx Proxy Manager"
+description: "An open-source reverse proxy manager"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - nginx
+  - reverse-proxy
+  - web
+---
 volumes:
   nginxproxymanager-data:
   nginxproxymanager-ssl:

+ 10 - 0
library/compose/nodeexporter/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Node Exporter"
+description: "A Prometheus exporter for hardware and OS metrics"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - prometheus
+  - monitoring
+  - metrics
+---
 services:
   node_exporter:
     image: quay.io/prometheus/node-exporter:v1.9.1

+ 0 - 14
library/compose/nvidiadgcm/compose.yaml

@@ -1,14 +0,0 @@
----
-services:
-  nvidia_exporter:
-    image: nvcr.io/nvidia/k8s/dcgm-exporter:2.3.2-2.6.2-ubuntu20.04
-    container_name: nvidia_exporter
-    runtime: nvidia
-    cap_add:
-      - SYS_ADMIN
-    environment:
-      - NVIDIA_VISIBLE_DEVICES=all
-      - NVIDIA_DRIVER_CAPABILITIES=all
-    ports:
-      - 9400:9400
-    restart: unless-stopped

+ 0 - 16
library/compose/nvidiasmi/compose.yaml

@@ -1,16 +0,0 @@
----
-services:
-  nvidia_smi_exporter:
-    image: docker.io/utkuozdemir/nvidia_gpu_exporter:1.3.2
-    container_name: nvidia_smi_exporter
-    runtime: nvidia
-    environment:
-      - NVIDIA_VISIBLE_DEVICES=all
-      - NVIDIA_DRIVER_CAPABILITIES=all
-    ports:
-      - "9835:9835"
-    volumes:
-      - /usr/bin/nvidia-smi:/usr/bin/nvidia-smi
-      - /usr/lib/x86_64-linux-gnu/libnvidia-ml.so:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so
-      - /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1
-    restart: unless-stopped

+ 10 - 0
library/compose/openwebui/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Open Web UI"
+description: "A web-based user interface for managing various services"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - openwebui
+  - web
+  - user-interface
+---
 services:
   openwebui:
     image: ghcr.io/open-webui/open-webui:v0.6.26

+ 10 - 0
library/compose/passbolt/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Passbolt"
+description: "An open-source password manager"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - passbolt
+  - password-manager
+  - web
+---
 volumes:
   passbolt-db:
   passbolt-data-gpg:

+ 10 - 0
library/compose/pihole/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Pi-hole"
+description: "An open-source DNS sinkhole"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - pihole
+  - dns
+  - ad-blocker
+---
 services:
   pihole:
     container_name: pihole

+ 10 - 0
library/compose/portainer/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Portainer"
+description: "An open-source container management tool"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - portainer
+  - container-management
+  - web
+---
 services:
   app:
     container_name: portainer

+ 10 - 0
library/compose/postgres/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "PostgreSQL"
+description: "An open-source relational database management system"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - postgres
+  - database
+  - sql
+---
 services:
   postgres:
     image: docker.io/library/postgres:17.6

+ 10 - 0
library/compose/prometheus/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Prometheus"
+description: "An open-source monitoring and alerting toolkit"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - prometheus
+  - monitoring
+  - alerting
+---
 volumes:
   prometheus-data:
     driver: local

+ 10 - 0
library/compose/promtail/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Promtail"
+description: "An open-source log collection agent"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - promtail
+  - logging
+  - grafana
+---
 services:
   promtail:
     image: docker.io/grafana/promtail:3.5.3

+ 0 - 36
library/compose/swag/compose.yaml

@@ -1,36 +0,0 @@
----
-services:
-  mariadb:
-    image: docker.io/linuxserver/mariadb:10.11.10
-    container_name: mariadb
-    environment:
-      - PUID=1001
-      - PGID=1001
-      - MYSQL_ROOT_PASSWORD=mariadbpassword
-      - TZ=Europe/Berlin
-      - MYSQL_DATABASE=WP_database
-      - MYSQL_USER=WP_dbuser
-      - MYSQL_PASSWORD=WP_dbpassword
-    volumes:
-      - /opt/webserver_swag/config/mariadb:/config
-    restart: unless-stopped
-  swag:
-    image: docker.io/linuxserver/swag:3.3.0
-    container_name: swag
-    cap_add:
-      - NET_ADMIN
-    environment:
-      - PUID=1001
-      - PGID=1001
-      - TZ=Europe/Berlin
-      - URL=do-test-1.the-digital-life.com
-      - SUBDOMAINS=
-      - VALIDATION=http
-    volumes:
-      - /opt/webserver_swag/config:/config
-    ports:
-      - 443:443
-      - 80:80  # optional
-    depends_on:
-      - mariadb
-    restart: unless-stopped

+ 10 - 0
library/compose/teleport/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Teleport"
+description: "An open-source access plane for managing SSH access"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - teleport
+  - ssh
+  - access-management
+---
 # -- (Optional) When using Traefik, use this section
 # networks:
 #   your-traefik-network:

+ 10 - 0
library/compose/traefik/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Traefik"
+description: "An open-source edge router for microservices"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - traefik
+  - reverse-proxy
+  - load-balancer
+---
 services:
   traefik:
     image: docker.io/library/traefik:v3.5.1

+ 10 - 0
library/compose/twingate_connector/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Twingate Connector"
+description: "A connector for Twingate"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - twingate
+  - connector
+  - networking
+---
 services:
   twingate_connector:
     container_name: twingate_connector

+ 10 - 0
library/compose/uptimekuma/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Uptime Kuma"
+description: "A self-hosted status monitoring solution"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - uptime-kuma
+  - monitoring
+  - self-hosted
+---
 volumes:
   uptimekuma-data:
     driver: local

+ 10 - 0
library/compose/wazuh/compose.yaml

@@ -1,4 +1,14 @@
 ---
+name: "Wazuh"
+description: "A security monitoring platform"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
+tags:
+  - wazuh
+  - security
+  - monitoring
+---
 services:
   wazuh.manager:
     image: docker.io/wazuh/wazuh-manager:4.12.0

+ 3 - 3
library/compose/whoami/compose.yaml

@@ -1,14 +1,14 @@
 ---
 name: "Whoami"
 description: "Simple HTTP service that returns information about the request"
-version: "1.0.0"
-author: "Your Name"
+version: "0.0.1"
+date: "2023-10-01"
+author: "Christian Lempa"
 tags:
   - "traefik"
   - "whoami"
   - "http"
   - "testing"
-category: "utilities"
 ---
 services:
   {{ service_name | default('whoami') }}: