xcad před 3 měsíci
rodič
revize
d0f5656db3

+ 0 - 5
CHANGELOG.md

@@ -41,11 +41,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Docker Swarm Volume Configuration
   - Support for local, mount, and NFS storage backends
   - Configurable NFS server, paths, and mount options
-- Storage Configuration for Docker Compose
-  - New `storage` section in compose module spec
-  - New `config` section in compose module spec
-  - Support for multiple storage backends: local, mount, nfs, glusterfs
-  - Dedicated configuration options for each backend type
 
 ### Changed
 - Compose module schema version bumped to "1.1"

+ 51 - 2
cli/core/collection.py

@@ -303,8 +303,12 @@ class VariableCollection:
                 dep_var, expected_value = self._parse_need(dep)
                 if expected_value is not None:  # Only validate new format
                     if dep_var not in self._variable_map:
-                        raise ValueError(
-                            f"Variable '{var_name}' has need '{dep}', but variable '{dep_var}' does not exist"
+                        # NOTE: We only warn here, not raise an error, because the variable might be
+                        # added later during merge with module spec. The actual runtime check in
+                        # _is_need_satisfied() will handle missing variables gracefully.
+                        logger.debug(
+                            f"Variable '{var_name}' has need '{dep}', but variable '{dep_var}' "
+                            f"not found (might be added during merge)"
                         )
 
         # Check for circular dependencies using depth-first search
@@ -396,6 +400,48 @@ class VariableCollection:
 
         return True
 
+    def reset_disabled_bool_variables(self) -> list[str]:
+        """Reset bool variables with unsatisfied dependencies to False.
+        
+        This ensures that disabled bool variables don't accidentally remain True
+        and cause confusion in templates or configuration.
+        
+        Returns:
+            List of variable names that were reset
+        """
+        reset_vars = []
+        
+        for section_key, section in self._sections.items():
+            # Check if section dependencies are satisfied
+            section_satisfied = self.is_section_satisfied(section_key)
+            is_enabled = section.is_enabled()
+            
+            for var_name, variable in section.variables.items():
+                # Only process bool variables
+                if variable.type != "bool":
+                    continue
+                    
+                # Check if variable's own dependencies are satisfied
+                var_satisfied = self.is_variable_satisfied(var_name)
+                
+                # 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:
+                    # 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
+                    if variable.value is not False:
+                        variable.value = False
+                        reset_vars.append(var_name)
+                        logger.debug(
+                            f"Reset disabled bool variable '{var_name}' to False "
+                            f"(section satisfied: {section_satisfied}, enabled: {is_enabled}, "
+                            f"var satisfied: {var_satisfied})"
+                        )
+        
+        return reset_vars
+
     def sort_sections(self) -> None:
         """Sort sections with the following priority:
 
@@ -749,6 +795,9 @@ class VariableCollection:
             for var_name, variable in section.variables.items():
                 merged._variable_map[var_name] = variable
 
+        # Validate dependencies after merge is complete
+        merged._validate_dependencies()
+
         return merged
 
     def _merge_sections(

+ 18 - 17
cli/core/display.py

@@ -228,18 +228,17 @@ class DisplayManager:
         console.print(table)
 
     def display_template_details(
-        self, template: Template, template_id: str, show_all: bool = False
+        self, template: Template, template_id: str
     ) -> None:
         """Display template information panel and variables table.
 
         Args:
             template: Template instance to display
             template_id: ID of the template
-            show_all: If True, show all variables/sections regardless of needs satisfaction
         """
         self._display_template_header(template, template_id)
         self._display_file_tree(template)
-        self._display_variables_table(template, show_all=show_all)
+        self._display_variables_table(template)
 
     def display_section_header(self, title: str, description: str | None) -> None:
         """Display a section header."""
@@ -455,13 +454,15 @@ class DisplayManager:
             console.print(file_tree)
 
     def _display_variables_table(
-        self, template: Template, show_all: bool = False
+        self, template: Template
     ) -> None:
         """Display a table of variables for a template.
 
+        All variables and sections are always shown. Disabled sections/variables
+        are displayed with dimmed styling.
+
         Args:
             template: Template instance
-            show_all: If True, show all variables/sections regardless of needs satisfaction
         """
         if not (template.variables and template.variables.has_sections()):
             return
@@ -480,12 +481,6 @@ class DisplayManager:
             if not section.variables:
                 continue
 
-            # Skip sections with unsatisfied needs unless show_all is True
-            if not show_all and not template.variables.is_section_satisfied(
-                section.key
-            ):
-                continue
-
             if not first_section:
                 variables_table.add_row("", "", "", "", style="bright_black")
             first_section = False
@@ -526,17 +521,23 @@ class DisplayManager:
                 # Check if variable's needs are satisfied
                 var_satisfied = template.variables.is_variable_satisfied(var_name)
 
-                # Skip variables with unsatisfied needs unless show_all is True
-                if not show_all and not var_satisfied:
-                    continue
-
                 # Dim the variable if section is dimmed OR variable needs are not satisfied
                 row_style = "bright_black" if (is_dimmed or not var_satisfied) else None
 
                 # Build default value display
+                # Special case: disabled bool variables show as "original → False"
+                if (is_dimmed or not var_satisfied) and variable.type == "bool":
+                    # Show that disabled bool variables are forced to False
+                    if hasattr(variable, "_original_disabled") and variable._original_disabled is not False:
+                        orig_val = str(variable._original_disabled)
+                        default_val = f"{orig_val} {IconManager.arrow_right()} False"
+                    else:
+                        default_val = "False"
                 # If origin is 'config' and original value differs from current, show: original → config_value
-                if (
-                    variable.origin == "config"
+                # BUT only for enabled variables (don't show arrow for disabled ones)
+                elif (
+                    not (is_dimmed or not var_satisfied)
+                    and variable.origin == "config"
                     and hasattr(variable, "_original_stored")
                     and variable.original_value != variable.value
                 ):

+ 14 - 15
cli/core/module.py

@@ -217,11 +217,6 @@ class Module(ABC):
     def show(
         self,
         id: str,
-        all_vars: bool = Option(
-            False,
-            "--all",
-            help="Show all variables/sections, even those with unsatisfied needs",
-        ),
     ) -> None:
         """Show template details."""
         logger.debug(f"Showing template '{id}' from module '{self.name}'")
@@ -254,8 +249,13 @@ class Module(ABC):
 
             # Re-sort sections after applying config (toggle values may have changed)
             template.variables.sort_sections()
+            
+            # Reset disabled bool variables to False to prevent confusion
+            reset_vars = template.variables.reset_disabled_bool_variables()
+            if reset_vars:
+                logger.debug(f"Reset {len(reset_vars)} disabled bool variables to False")
 
-        self._display_template_details(template, id, show_all=all_vars)
+        self._display_template_details(template, id)
 
     def _apply_variable_defaults(self, template: Template) -> None:
         """Apply config defaults and CLI overrides to template variables.
@@ -610,11 +610,6 @@ class Module(ABC):
         quiet: bool = Option(
             False, "--quiet", "-q", help="Suppress all non-error output"
         ),
-        all_vars: bool = Option(
-            False,
-            "--all",
-            help="Show all variables/sections, even those with unsatisfied needs",
-        ),
     ) -> None:
         """Generate from template.
 
@@ -656,9 +651,14 @@ class Module(ABC):
         # Re-sort sections after all overrides (toggle values may have changed)
         if template.variables:
             template.variables.sort_sections()
+            
+            # Reset disabled bool variables to False to prevent confusion
+            reset_vars = template.variables.reset_disabled_bool_variables()
+            if reset_vars:
+                logger.debug(f"Reset {len(reset_vars)} disabled bool variables to False")
 
         if not quiet:
-            self._display_template_details(template, id, show_all=all_vars)
+            self._display_template_details(template, id)
             console.print()
 
         # Collect variable values
@@ -1285,13 +1285,12 @@ class Module(ABC):
             ) from exc
 
     def _display_template_details(
-        self, template: Template, id: str, show_all: bool = False
+        self, template: Template, id: str
     ) -> None:
         """Display template information panel and variables table.
 
         Args:
             template: Template instance to display
             id: Template ID
-            show_all: If True, show all variables/sections regardless of needs satisfaction
         """
-        self.display.display_template_details(template, id, show_all=show_all)
+        self.display.display_template_details(template, id)

+ 1 - 1
cli/modules/compose/spec_v1_0.py

@@ -174,7 +174,7 @@ spec = OrderedDict(
                 },
                 "database_external": {
                     "description": "Use an external database server?",
-                    "extra": "If 'no', a database container will be created in the compose project.",
+                    "extra": "skips creation of internal database container",
                     "type": "bool",
                     "default": False,
                 },

+ 7 - 7
cli/modules/compose/spec_v1_1.py

@@ -64,11 +64,10 @@ spec = OrderedDict(
                     "default": False,
                 },
                 "network_mode": {
-                    "description": "Docker network mode",
+                    "description": "Docker network mode (bridge, host, or macvlan)",
                     "type": "enum",
                     "options": ["bridge", "host", "macvlan"],
                     "default": "bridge",
-                    "extra": "bridge=default Docker networking, host=use host network stack, macvlan=dedicated MAC address on physical network",
                 },
                 "network_name": {
                     "description": "Docker network name",
@@ -77,7 +76,7 @@ spec = OrderedDict(
                     "needs": "network_mode=bridge,macvlan",
                 },
                 "network_external": {
-                    "description": "Use existing Docker network",
+                    "description": "Use existing Docker network (external)",
                     "type": "bool",
                     "default": True,
                     "needs": "network_mode=bridge,macvlan",
@@ -111,6 +110,7 @@ spec = OrderedDict(
         "ports": {
             "title": "Ports",
             "toggle": "ports_enabled",
+            "needs": "network_mode=bridge",
             "vars": {
                 "ports_enabled": {
                     "description": "Expose ports via 'ports' mapping",
@@ -122,6 +122,7 @@ spec = OrderedDict(
         "traefik": {
             "title": "Traefik",
             "toggle": "traefik_enabled",
+            "needs": "network_mode=bridge",
             "description": "Traefik routes external traffic to your service.",
             "vars": {
                 "traefik_enabled": {
@@ -148,7 +149,7 @@ spec = OrderedDict(
         "traefik_tls": {
             "title": "Traefik TLS/SSL",
             "toggle": "traefik_tls_enabled",
-            "needs": "traefik_enabled=true",
+            "needs": "traefik_enabled=true;network_mode=bridge",
             "description": "Enable HTTPS/TLS for Traefik with certificate management.",
             "vars": {
                 "traefik_tls_enabled": {
@@ -239,10 +240,9 @@ spec = OrderedDict(
         "database": {
             "title": "Database",
             "toggle": "database_enabled",
-            "description": "Connect to external database (PostgreSQL or MySQL)",
             "vars": {
                 "database_enabled": {
-                    "description": "Enable external database integration",
+                    "description": "Enable database configuration",
                     "type": "bool",
                     "default": False,
                 },
@@ -254,7 +254,7 @@ spec = OrderedDict(
                 },
                 "database_external": {
                     "description": "Use an external database server?",
-                    "extra": "If 'no', a database container will be created in the compose project.",
+                    "extra": "skips creation of internal database container",
                     "type": "bool",
                     "default": False,
                 },

+ 29 - 0
library/compose/pihole/.env.pihole.j2

@@ -0,0 +1,29 @@
+# Pi-hole Configuration
+# Contains sensitive application secrets and configuration
+
+# Timezone
+TZ={{ container_timezone }}
+
+# User and Group IDs
+PIHOLE_UID={{ user_uid }}
+PIHOLE_GID={{ user_gid }}
+
+# Web Interface Admin Password
+FTLCONF_webserver_api_password={{ pihole_webpassword }}
+
+# Upstream DNS Servers
+FTLCONF_dns_upstreams={{ pihole_dns_upstreams }}
+
+# DNS Listening Mode
+{% if dns_enabled -%}
+FTLCONF_dns_listeningMode=all
+{% else -%}
+FTLCONF_dns_listeningMode=local
+{% endif %}
+
+# DHCP Server
+{% if dhcp_enabled -%}
+FTLCONF_dhcp_active=true
+{% else -%}
+FTLCONF_dhcp_active=false
+{% endif %}

+ 7 - 6
library/compose/pihole/compose.yaml.j2

@@ -2,6 +2,8 @@ services:
   {{ service_name }}:
     container_name: {{ container_name }}
     image: docker.io/pihole/pihole:2025.08.0
+    env_file:
+      - .env.pihole
     {% if network_mode == 'host' %}
     network_mode: host
     {% elif traefik_enabled or network_mode == 'macvlan' %}
@@ -16,10 +18,9 @@ services:
       {{ network_name }}:
       {% endif %}
     {% endif %}
-    {% if ports_enabled and network_mode not in ['host', 'macvlan'] and (not traefik_enabled or dns_enabled or dhcp_enabled) %}
+    {% if network_mode not in ['host', 'macvlan'] and (not traefik_enabled or dns_enabled or dhcp_enabled) %}
     ports:
       {% if not traefik_enabled %}
-      - "{{ ports_http }}:80/tcp"
       - "{{ ports_https }}:443/tcp"
       {% endif %}
       {% if dns_enabled %}
@@ -30,13 +31,13 @@ services:
       - "67:67/udp"
       {% endif %}
     {% endif %}
-    environment:
-      - TZ={{ container_timezone }}
-      {% if pihole_webpassword %}      - FTLCONF_webserver_api_password={{ pihole_webpassword }}
-      {% endif %}      - FTLCONF_dns_upstreams={{ pihole_dns_upstreams }}
     volumes:
       - config_dnsmasq:/etc/dnsmasq.d
       - config_pihole:/etc/pihole
+    {% if dhcp_enabled %}
+    cap_add:
+      - NET_ADMIN
+    {% endif %}
     {% if traefik_enabled %}
     labels:
       - traefik.enable=true

+ 8 - 9
library/compose/pihole/template.yaml

@@ -30,10 +30,11 @@ metadata:
        {% elif traefik_enabled -%}
        {% if traefik_tls_enabled %}https{% else %}http{% endif %}://{{ traefik_host }}
        {%- elif ports_enabled -%}
-       http://localhost:{{ ports_http }}
+       http://localhost:{{ ports_http }}/admin/login
        {%- endif %}
 
     3. Login password: {{ pihole_webpassword }}
+       (stored in .env.pihole file)
 
     {% if network_enabled and network_mode == 'macvlan' -%}
     4. Configure devices to use {{ network_macvlan_ipv4_address }} as DNS server
@@ -70,9 +71,9 @@ spec:
         extra: "Exposes port 53 for DNS queries in bridge network mode"
       dhcp_enabled:
         type: bool
-        description: "Enable DHCP server functionality"
-        default: false
-        extra: "Exposes port 67 for DHCP in bridge network mode"
+        needs: "network_mode=host,macvlan"
+        description: "Enable DHCP server functionality (requires host or macvlan network mode)"
+        default: true
   traefik:
     vars:
       traefik_host:
@@ -85,12 +86,10 @@ spec:
       network_external:
         default: false
   ports:
+    needs: "network_mode=bridge"
     vars:
-      ports_enabled:
-        extra: "Only required in bridge network mode without Traefik"
-      ports_http:
-        type: int
-        default: 8080
       ports_https:
+        description: "HTTPS port for web interface"
         type: int
         default: 8443
+        extra: "Only used if Traefik is not enabled"