Przeglądaj źródła

new release + pihole template

xcad 8 miesięcy temu
rodzic
commit
27df602f72

+ 41 - 6
AGENTS.md

@@ -67,10 +67,15 @@ The project is stored in a public GitHub Repository, use issues, and branches fo
 
 
 ### Modules
 ### Modules
 
 
+**Module Structure:**
+Modules can be either single files or packages:
+- **Single file**: `cli/modules/modulename.py` (for simple modules)
+- **Package**: `cli/modules/modulename/` with `__init__.py` (for multi-schema modules)
+
 **Creating Modules:**
 **Creating Modules:**
 - Subclass `Module` from `cli/core/module.py`
 - Subclass `Module` from `cli/core/module.py`
-- Define `name` and `description` class attributes
-- Optional: Define module-wide `spec` for default variables (common across all templates)
+- Define `name`, `description`, and `schema_version` class attributes
+- For multi-schema modules: organize specs in separate files (e.g., `spec_v1_0.py`, `spec_v1_1.py`)
 - Call `registry.register(YourModule)` at module bottom
 - Call `registry.register(YourModule)` at module bottom
 - Auto-discovered and registered at CLI startup
 - Auto-discovered and registered at CLI startup
 
 
@@ -83,8 +88,19 @@ spec = VariableCollection.from_dict({
 })
 })
 ```
 ```
 
 
+**Multi-Schema Modules:**
+For modules supporting multiple schema versions, use package structure:
+```
+cli/modules/compose/
+  __init__.py          # Module class, loads appropriate spec
+  spec_v1_0.py         # Schema 1.0 specification
+  spec_v1_1.py         # Schema 1.1 specification
+```
+
 **Existing Modules:**
 **Existing Modules:**
-- `cli/modules/compose.py` - Docker Compose (defines extensive module spec with traefik, database, email, authentik sections)
+- `cli/modules/compose/` - Docker Compose package with schema 1.0 and 1.1 support
+  - `spec_v1_0.py` - Basic compose spec
+  - `spec_v1_1.py` - Extended with network_mode, swarm support
 
 
 **(Work in Progress):** terraform, docker, ansible, kubernetes, packer modules
 **(Work in Progress):** terraform, docker, ansible, kubernetes, packer modules
 
 
@@ -185,7 +201,8 @@ spec:
 ```
 ```
 
 
 **How It Works:**
 **How It Works:**
-- **Module Schema Version**: Each module (in `cli/modules/*.py`) defines `schema_version` (e.g., "1.0")
+- **Module Schema Version**: Each module defines `schema_version` (e.g., "1.1")
+- **Module Spec Loading**: Modules load appropriate spec based on template's schema version
 - **Template Schema Version**: Each template declares `schema` at the top level (defaults to "1.0")
 - **Template Schema Version**: Each template declares `schema` at the top level (defaults to "1.0")
 - **Compatibility Check**: Template schema ≤ Module schema → Compatible
 - **Compatibility Check**: Template schema ≤ Module schema → Compatible
 - **Incompatibility**: Template schema > Module schema → `IncompatibleSchemaVersionError`
 - **Incompatibility**: Template schema > Module schema → `IncompatibleSchemaVersionError`
@@ -201,12 +218,30 @@ spec:
 - Set template schema when using features from a specific schema
 - Set template schema when using features from a specific schema
 - Example: Template using new variable type added in schema 1.1 should set `schema: "1.1"`
 - Example: Template using new variable type added in schema 1.1 should set `schema: "1.1"`
 
 
-**Module Example:**
+**Single-File Module Example:**
 ```python
 ```python
+class SimpleModule(Module):
+  name = "simple"
+  description = "Simple module"
+  schema_version = "1.0"
+  spec = VariableCollection.from_dict({...})  # Single spec
+```
+
+**Multi-Schema Module Example:**
+```python
+# cli/modules/compose/__init__.py
 class ComposeModule(Module):
 class ComposeModule(Module):
   name = "compose"
   name = "compose"
   description = "Manage Docker Compose configurations"
   description = "Manage Docker Compose configurations"
-  schema_version = "1.0"  # Current schema version supported
+  schema_version = "1.1"  # Highest schema version supported
+  
+  def get_spec(self, template_schema: str) -> VariableCollection:
+    """Load spec based on template schema version."""
+    if template_schema == "1.0":
+      from .spec_v1_0 import get_spec
+    elif template_schema == "1.1":
+      from .spec_v1_1 import get_spec
+    return get_spec()
 ```
 ```
 
 
 **Version Management:**
 **Version Management:**

+ 17 - 38
CHANGELOG.md

@@ -8,46 +8,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ## [Unreleased]
 ## [Unreleased]
 
 
 ### Added
 ### Added
-- Multi-Schema Module Support
-  - Modules can now maintain multiple schema versions simultaneously
-  - Schema specs organized in separate files (e.g., `spec_v1_0.py`, `spec_v1_1.py`)
-  - CLI automatically uses appropriate schema based on template declaration
-  - Module discovery now supports both file and package modules
-- Compose Schema 1.1 Network Enhancements
-  - Added `network_mode` with options: bridge, host, macvlan
-  - Macvlan support with conditional fields (IP address, interface, subnet, gateway)
-  - Host mode support for direct host network access
-  - Network fields conditionally shown based on selected mode
-- Comma-Separated Values in Dependencies
-  - `needs` now supports multiple values: `network_mode=bridge,macvlan`
-  - Variable shown if actual value matches ANY of the specified values
-- Template Schema Versioning (#1360)
-  - Templates can now declare schema version (defaults to "1.0" for backward compatibility)
-  - Modules validate template compatibility against supported schema version
-  - Incompatible templates show clear error with upgrade instructions
-  - New `cli/core/version.py` module for semantic version comparison
-  - New `IncompatibleSchemaVersionError` exception for version mismatches
-- Variable-level Dependencies
-  - Variables can now have `needs` dependencies with format `variable=value`
-  - Sections support new dependency format: `needs: "variable=value"`
-  - Backward compatible with old section-only dependencies (`needs: "section_name"`)
-  - `is_variable_satisfied()` method added to VariableCollection
-- Show/Generate `--all` flag
-  - Added `--all` flag to `show` and `generate` commands
-  - Shows all variables/sections regardless of needs satisfaction
-  - Useful for debugging and viewing complete template structure
-- Optional Variables
-  - Variables can now be marked with `optional: true` to allow empty/None values
-- Docker Swarm Volume Configuration
-  - Support for local, mount, and NFS storage backends
-  - Configurable NFS server, paths, and mount options
+- Multiple Library Support (#1314) for git and local libraries
+- Multi-Schema Module Support and Backward Compatibility (Schema-1.0)
+- Schema-1.1 `network_mode` with options: bridge, host, macvlan
+- Schema-1.1 `swarm` module support
+- Variable-level and Section-level depenendencies `needs` with multiple values support
+- Optional Variables `optional: true` to allow empty/None values
+- PEP 8 formatting alignment
+- CLI variable dependency validation - raises error when CLI-provided variables have unsatisfied dependencies
 
 
 ### Changed
 ### Changed
-- Compose module schema version bumped to "1.1"
-- Traefik TLS section now uses variable-level dependencies (`needs: "traefik_enabled=true"`)
-- Display manager hides sections/variables with unsatisfied needs by default (unless `--all` flag is used)
-- Variables with unsatisfied needs are dimmed when shown with `--all` flag
-- Dependency validation now supports both old (section) and new (variable=value) formats
+- Schema-1.1 Unified Docker Swarm Placement (#1359) - Simplified swarm placement constraints into a single variable
+- Refactored compose module from single file to package structure
+- Dependency validation moved to `validate_all()` for better error reporting
+- Schema-1.1 removed `network_enabled`, `ports_enabled` and `database_enabled` toggles (no longer optional)
+
+### Fixed
+- Required sections now ignore toggle and are always enabled
+- Module spec loading based on correct template schema version
+- Interactive prompts now skip all variables (including required) when parent section is disabled
 
 
 ## [0.0.6] - 2025-01-XX
 ## [0.0.6] - 2025-01-XX
 
 

+ 53 - 11
cli/core/collection.py

@@ -95,7 +95,7 @@ class VariableCollection:
             vars_data = {}
             vars_data = {}
 
 
         for var_name, var_data in vars_data.items():
         for var_name, var_data in vars_data.items():
-            var_init_data = {"name": var_name, **var_data}
+            var_init_data = {"name": var_name, "parent_section": section, **var_data}
             variable = Variable(var_init_data)
             variable = Variable(var_init_data)
             section.variables[var_name] = variable
             section.variables[var_name] = variable
             # NOTE: Populate the direct lookup map for efficient access.
             # NOTE: Populate the direct lookup map for efficient access.
@@ -406,6 +406,9 @@ class VariableCollection:
         This ensures that disabled bool variables don't accidentally remain True
         This ensures that disabled bool variables don't accidentally remain True
         and cause confusion in templates or configuration.
         and cause confusion in templates or configuration.
         
         
+        Note: CLI-provided variables are NOT reset here - they are validated
+        later in validate_all() to provide better error messages.
+        
         Returns:
         Returns:
             List of variable names that were reset
             List of variable names that were reset
         """
         """
@@ -426,12 +429,16 @@ class VariableCollection:
                 
                 
                 # If section is disabled OR variable dependencies aren't met, reset to False
                 # If section is disabled OR variable dependencies aren't met, reset to False
                 if not section_satisfied or not is_enabled or not var_satisfied:
                 if not section_satisfied or not is_enabled or not var_satisfied:
-                    # Store original value if not already stored (for display purposes)
-                    if not hasattr(variable, "_original_disabled"):
-                        variable._original_disabled = variable.value
-                    
                     # Only reset if current value is not already False
                     # Only reset if current value is not already False
                     if variable.value is not False:
                     if variable.value is not False:
+                        # Don't reset CLI-provided variables - they'll be validated later
+                        if variable.origin == "cli":
+                            continue
+                        
+                        # Store original value if not already stored (for display purposes)
+                        if not hasattr(variable, "_original_disabled"):
+                            variable._original_disabled = variable.value
+                        
                         variable.value = False
                         variable.value = False
                         reset_vars.append(var_name)
                         reset_vars.append(var_name)
                         logger.debug(
                         logger.debug(
@@ -676,9 +683,38 @@ class VariableCollection:
         Validates:
         Validates:
         - All variables in enabled sections with satisfied dependencies
         - All variables in enabled sections with satisfied dependencies
         - Required variables even if their section is disabled (but dependencies must be satisfied)
         - Required variables even if their section is disabled (but dependencies must be satisfied)
+        - CLI-provided bool variables with unsatisfied dependencies
         """
         """
         errors: list[str] = []
         errors: list[str] = []
 
 
+        # First, check for CLI-provided bool variables with unsatisfied dependencies
+        for section_key, section in self._sections.items():
+            section_satisfied = self.is_section_satisfied(section_key)
+            is_enabled = section.is_enabled()
+            
+            for var_name, variable in section.variables.items():
+                # Check CLI-provided bool variables with unsatisfied dependencies
+                if variable.type == "bool" and variable.origin == "cli" and variable.value is not False:
+                    var_satisfied = self.is_variable_satisfied(var_name)
+                    
+                    if not section_satisfied or not is_enabled or not var_satisfied:
+                        # Build error message with unmet needs (use set to avoid duplicates)
+                        unmet_needs = set()
+                        if not section_satisfied:
+                            for need in section.needs:
+                                if not self._is_need_satisfied(need):
+                                    unmet_needs.add(need)
+                        if not var_satisfied:
+                            for need in variable.needs:
+                                if not self._is_need_satisfied(need):
+                                    unmet_needs.add(need)
+                        
+                        needs_str = ", ".join(sorted(unmet_needs)) if unmet_needs else "dependencies not satisfied"
+                        errors.append(
+                            f"{section.key}.{var_name} (set via CLI to {variable.value} but requires: {needs_str})"
+                        )
+
+        # Then validate all other variables
         for section_key, section in self._sections.items():
         for section_key, section in self._sections.items():
             # Skip sections with unsatisfied dependencies (even for required variables)
             # Skip sections with unsatisfied dependencies (even for required variables)
             if not self.is_section_satisfied(section_key):
             if not self.is_section_satisfied(section_key):
@@ -697,8 +733,9 @@ class VariableCollection:
 
 
             # Validate variables in the section
             # Validate variables in the section
             for var_name, variable in section.variables.items():
             for var_name, variable in section.variables.items():
-                # Skip non-required variables in disabled sections
-                if not is_enabled and not variable.required:
+                # Skip all variables (including required ones) in disabled sections
+                # Required variables are only required when their section is actually enabled
+                if not is_enabled:
                     continue
                     continue
 
 
                 try:
                 try:
@@ -807,15 +844,14 @@ class VariableCollection:
         merged_section = self_section.clone()
         merged_section = self_section.clone()
 
 
         # Update section metadata from other (other takes precedence)
         # Update section metadata from other (other takes precedence)
-        # Only override if explicitly provided in other AND has a value
+        # Explicit null/empty values clear the property (reset mechanism)
         for attr in ("title", "description", "toggle"):
         for attr in ("title", "description", "toggle"):
-            other_value = getattr(other_section, attr)
             if (
             if (
                 hasattr(other_section, "_explicit_fields")
                 hasattr(other_section, "_explicit_fields")
                 and attr in other_section._explicit_fields
                 and attr in other_section._explicit_fields
-                and other_value
             ):
             ):
-                setattr(merged_section, attr, other_value)
+                # Set to the other value even if null/empty (enables explicit reset)
+                setattr(merged_section, attr, getattr(other_section, attr))
 
 
         merged_section.required = other_section.required
         merged_section.required = other_section.required
         # Respect explicit clears for dependencies (explicit null/empty clears, missing field preserves)
         # Respect explicit clears for dependencies (explicit null/empty clears, missing field preserves)
@@ -855,6 +891,12 @@ class VariableCollection:
                     if bool_field in other_var._explicit_fields:
                     if bool_field in other_var._explicit_fields:
                         update[bool_field] = getattr(other_var, bool_field)
                         update[bool_field] = getattr(other_var, bool_field)
 
 
+                # Special handling for needs (allow explicit null/empty to clear)
+                if "needs" in other_var._explicit_fields:
+                    update["needs"] = (
+                        other_var.needs.copy() if other_var.needs else []
+                    )
+
                 # Special handling for value/default (allow explicit null to clear)
                 # Special handling for value/default (allow explicit null to clear)
                 if "value" in other_var._explicit_fields:
                 if "value" in other_var._explicit_fields:
                     update["value"] = other_var.value
                     update["value"] = other_var.value

+ 3 - 3
cli/core/prompt.py

@@ -110,10 +110,10 @@ class PromptHandler:
                     )
                     )
                     continue
                     continue
 
 
-                # Skip non-required variables if section is disabled
-                if not section_will_be_enabled and not variable.required:
+                # Skip all variables if section is disabled
+                if not section_will_be_enabled:
                     logger.debug(
                     logger.debug(
-                        f"Skipping non-required variable '{var_name}' from disabled section '{section_key}'"
+                        f"Skipping variable '{var_name}' from disabled section '{section_key}'"
                     )
                     )
                     continue
                     continue
 
 

+ 11 - 0
cli/core/variable.py

@@ -37,6 +37,8 @@ class Variable:
 
 
         # Initialize fields
         # Initialize fields
         self.name: str = data["name"]
         self.name: str = data["name"]
+        # Reference to parent section (set by VariableCollection)
+        self.parent_section: Optional["VariableSection"] = data.get("parent_section")
         self.description: Optional[str] = data.get("description") or data.get(
         self.description: Optional[str] = data.get("description") or data.get(
             "display", ""
             "display", ""
         )
         )
@@ -404,6 +406,14 @@ class Variable:
         # No default value and not autogenerated = required
         # No default value and not autogenerated = required
         return True
         return True
 
 
+    def get_parent(self) -> Optional["VariableSection"]:
+        """Get the parent VariableSection that contains this variable.
+        
+        Returns:
+            The parent VariableSection if set, None otherwise
+        """
+        return self.parent_section
+
     def clone(self, update: Optional[Dict[str, Any]] = None) -> "Variable":
     def clone(self, update: Optional[Dict[str, Any]] = None) -> "Variable":
         """Create a deep copy of the variable with optional field updates.
         """Create a deep copy of the variable with optional field updates.
 
 
@@ -433,6 +443,7 @@ class Variable:
             "optional": self.optional,
             "optional": self.optional,
             "original_value": self.original_value,
             "original_value": self.original_value,
             "needs": self.needs.copy() if self.needs else None,
             "needs": self.needs.copy() if self.needs else None,
+            "parent_section": self.parent_section,
         }
         }
 
 
         # Apply updates if provided
         # Apply updates if provided

+ 5 - 21
cli/modules/compose/spec_v1_1.py

@@ -56,13 +56,7 @@ spec = OrderedDict(
         },
         },
         "network": {
         "network": {
             "title": "Network",
             "title": "Network",
-            "toggle": "network_enabled",
             "vars": {
             "vars": {
-                "network_enabled": {
-                    "description": "Enable custom network block",
-                    "type": "bool",
-                    "default": False,
-                },
                 "network_mode": {
                 "network_mode": {
                     "description": "Docker network mode",
                     "description": "Docker network mode",
                     "type": "enum",
                     "type": "enum",
@@ -78,7 +72,7 @@ spec = OrderedDict(
                 "network_external": {
                 "network_external": {
                     "description": "Use existing Docker network (external)",
                     "description": "Use existing Docker network (external)",
                     "type": "bool",
                     "type": "bool",
-                    "default": True,
+                    "default": False,
                     "needs": "network_mode=bridge,macvlan",
                     "needs": "network_mode=bridge,macvlan",
                 },
                 },
                 "network_macvlan_ipv4_address": {
                 "network_macvlan_ipv4_address": {
@@ -112,11 +106,6 @@ spec = OrderedDict(
             "toggle": "ports_enabled",
             "toggle": "ports_enabled",
             "needs": "network_mode=bridge",
             "needs": "network_mode=bridge",
             "vars": {
             "vars": {
-                "ports_enabled": {
-                    "description": "Expose ports via 'ports' mapping",
-                    "type": "bool",
-                    "default": True,
-                }
             },
             },
         },
         },
         "traefik": {
         "traefik": {
@@ -171,6 +160,7 @@ spec = OrderedDict(
         },
         },
         "swarm": {
         "swarm": {
             "title": "Docker Swarm",
             "title": "Docker Swarm",
+            "needs": "network_mode=bridge",
             "toggle": "swarm_enabled",
             "toggle": "swarm_enabled",
             "description": "Deploy service in Docker Swarm mode.",
             "description": "Deploy service in Docker Swarm mode.",
             "vars": {
             "vars": {
@@ -184,7 +174,6 @@ spec = OrderedDict(
                     "type": "enum",
                     "type": "enum",
                     "options": ["replicated", "global"],
                     "options": ["replicated", "global"],
                     "default": "replicated",
                     "default": "replicated",
-                    "extra": "replicated=run specific number of tasks, global=run one task per node",
                 },
                 },
                 "swarm_replicas": {
                 "swarm_replicas": {
                     "description": "Number of replicas",
                     "description": "Number of replicas",
@@ -198,7 +187,7 @@ spec = OrderedDict(
                     "default": "",
                     "default": "",
                     "optional": True,
                     "optional": True,
                     "needs": "swarm_placement_mode=replicated",
                     "needs": "swarm_placement_mode=replicated",
-                    "extra": "Constrains service to run on specific node by hostname (optional)",
+                    "extra": "Constrains service to run on specific node by hostname",
                 },
                 },
                 "swarm_volume_mode": {
                 "swarm_volume_mode": {
                     "description": "Swarm volume storage backend",
                     "description": "Swarm volume storage backend",
@@ -241,16 +230,11 @@ spec = OrderedDict(
             "title": "Database",
             "title": "Database",
             "toggle": "database_enabled",
             "toggle": "database_enabled",
             "vars": {
             "vars": {
-                "database_enabled": {
-                    "description": "Enable database configuration",
-                    "type": "bool",
-                    "default": False,
-                },
                 "database_type": {
                 "database_type": {
                     "description": "Database type",
                     "description": "Database type",
                     "type": "enum",
                     "type": "enum",
-                    "options": ["postgres", "mysql"],
-                    "default": "postgres",
+                    "options": ["default", "sqlite", "postgres", "mysql"],
+                    "default": "default",
                 },
                 },
                 "database_external": {
                 "database_external": {
                     "description": "Use an external database server?",
                     "description": "Use an external database server?",

+ 9 - 4
library/compose/pihole/compose.yaml.j2

@@ -3,12 +3,12 @@ services:
     {% if not swarm_enabled %}
     {% if not swarm_enabled %}
     container_name: {{ container_name }}
     container_name: {{ container_name }}
     {% endif %}
     {% endif %}
-    image: docker.io/pihole/pihole:2025.08.0
+    image: docker.io/pihole/pihole:2025.10.2
     env_file:
     env_file:
       - .env.pihole
       - .env.pihole
-    {% if network_enabled == 'true' and network_mode == 'host' %}
+    {% if network_mode == 'host' %}
     network_mode: host
     network_mode: host
-    {% elif traefik_enabled or network_enabled == 'true' %}
+    {% else %}
     networks:
     networks:
       {% if traefik_enabled %}
       {% if traefik_enabled %}
       {{ traefik_network }}:
       {{ traefik_network }}:
@@ -24,11 +24,16 @@ services:
     ports:
     ports:
       {% if not traefik_enabled %}
       {% if not traefik_enabled %}
       {% if swarm_enabled %}
       {% if swarm_enabled %}
+      - target: 80
+        published: {{ ports_http }}
+        protocol: tcp
+        mode: host
       - target: 443
       - target: 443
         published: {{ ports_https }}
         published: {{ ports_https }}
         protocol: tcp
         protocol: tcp
         mode: host
         mode: host
       {% else %}
       {% else %}
+      - "{{ ports_http }}:80/tcp"
       - "{{ ports_https }}:443/tcp"
       - "{{ ports_https }}:443/tcp"
       {% endif %}
       {% endif %}
       {% endif %}
       {% endif %}
@@ -145,7 +150,7 @@ volumes:
     driver: local
     driver: local
 {% endif %}
 {% endif %}
 
 
-{% if network_mode != 'host' and (network_mode in ['bridge', 'macvlan'] or traefik_enabled) %}
+{% if network_mode != 'host' %}
 networks:
 networks:
   {% if network_mode == 'macvlan' %}
   {% if network_mode == 'macvlan' %}
   {{ network_name }}:
   {{ network_name }}:

+ 30 - 34
library/compose/pihole/template.yaml

@@ -14,45 +14,27 @@ metadata:
     Documentation: https://docs.pi-hole.net/
     Documentation: https://docs.pi-hole.net/
 
 
     GitHub: https://github.com/pi-hole/pi-hole
     GitHub: https://github.com/pi-hole/pi-hole
-  version: 2025.08.0
+  version: 2025.10.2
   author: Christian Lempa
   author: Christian Lempa
-  date: '2025-09-28'
+  date: '2025-10-28'
   tags:
   tags:
     - dns
     - dns
     - ad-blocking
     - ad-blocking
-  draft: false
   next_steps: |
   next_steps: |
     {% if swarm_enabled -%}
     {% if swarm_enabled -%}
-    1. Create Docker Swarm secret for admin password:
-       echo "{{ webpassword }}" | docker secret create {{ webpassword_secret_name }} -
-       (Or use the generated .env.secret file)
-
-    2. Deploy to Swarm:
+    1. Deploy to Docker Swarm:
        docker stack deploy -c compose.yaml pihole
        docker stack deploy -c compose.yaml pihole
-
-    3. Verify deployment:
-       docker service ls
-       docker service logs pihole_{{ service_name }}
-
-    4. Access web interface:
-       {% if network_mode == 'macvlan' -%}https://{{ network_macvlan_ipv4_address }}/admin/login
-       {%- elif traefik_enabled == True -%}https://{{ traefik_host }}/admin/login
-       {%- else -%}https://localhost:{{ ports_https }}/admin/login{%- endif %}
-
-    5. Login password: Stored in Docker secret '{{ webpassword_secret_name }}'
-
-    6. Configure devices to use your swarm node's IP address as DNS server
+    2. Access the Web Interface:
+       {%- if traefik_enabled == True -%}https://{{ traefik_host }}/admin
+       {%- else -%}https://<your-swarm-node-ip>:{{ ports_https }}/admin{%- endif %}
+    3. Configure devices to use swarm node IP as DNS
     {% else -%}
     {% else -%}
-    1. Start: docker compose up -d
-
-    2. Access web interface:
-       {% if network_mode == 'macvlan' -%}https://{{ network_macvlan_ipv4_address }}/admin/login
-       {%- elif traefik_enabled == True -%}https://{{ traefik_host }}/admin/login
-       {%- else -%}https://localhost:{{ ports_https }}/admin/login{%- endif %}
-
-    3. Login password: {{ webpassword }}
-
-    4. Configure devices to use your host's IP address as DNS server
+    1. Deploy to Docker:
+       docker compose up -d
+    2. Access the Web Interface:
+       {%- if traefik_enabled == True -%}https://{{ traefik_host }}/admin
+       {%- else -%}https://<your-docker-host-ip>:{{ ports_https }}/admin{%- endif %}
+    3. Configure devices to use docker host IP as DNS
     {% endif -%}
     {% endif -%}
 spec:
 spec:
   general:
   general:
@@ -73,34 +55,48 @@ spec:
         autogenerated: true
         autogenerated: true
   traefik:
   traefik:
     vars:
     vars:
+      traefik_enabled:
+        needs: "network_mode=bridge"
       traefik_host:
       traefik_host:
         default: "pihole.home.arpa"
         default: "pihole.home.arpa"
   network:
   network:
     vars:
     vars:
       network_mode:
       network_mode:
-        extra: "If you need DHCP functionality, use 'host' or 'macvlan' mode"
+        extra: >
+          If you need DHCP functionality, use 'host' or 'macvlan' mode.
+          NOTE: Swarm only supports 'bridge' mode!"
       network_name:
       network_name:
         default: "pihole_network"
         default: "pihole_network"
   ports:
   ports:
-    needs: "network_mode=bridge"
     vars:
     vars:
+      ports_http:
+        description: "External HTTP port"
+        type: int
+        default: 8080
+        needs: ["traefik_enabled=false", "network_mode=bridge"]
       ports_https:
       ports_https:
         description: "External HTTPS port"
         description: "External HTTPS port"
         type: int
         type: int
         default: 8443
         default: 8443
-        extra: "Only used if Traefik is not enabled"
+        needs: ["traefik_enabled=false", "network_mode=bridge"]
       ports_dns:
       ports_dns:
         description: "External DNS port"
         description: "External DNS port"
         type: int
         type: int
         default: 53
         default: 53
+        needs: "network_mode=bridge"
       ports_ntp:
       ports_ntp:
         description: "External NTP port"
         description: "External NTP port"
         type: int
         type: int
         default: 123
         default: 123
+        needs: "network_mode=bridge"
   swarm:
   swarm:
     vars:
     vars:
+      swarm_enabled:
+        needs: "network_mode=bridge"
       swarm_placement_host:
       swarm_placement_host:
         required: true
         required: true
+        optional: false
+        needs: null
       webpassword_secret_name:
       webpassword_secret_name:
         description: "Docker Swarm secret name for admin password"
         description: "Docker Swarm secret name for admin password"
         type: str
         type: str

+ 1 - 1
library/compose/traefik/template.yaml

@@ -57,7 +57,7 @@ metadata:
        - Review and limit network exposure
        - Review and limit network exposure
 
 
     For more information, visit: https://doc.traefik.io/traefik/
     For more information, visit: https://doc.traefik.io/traefik/
-  draft: false
+  draft: true
 spec:
 spec:
   general:
   general:
     title: "General"
     title: "General"