mirror of
https://github.com/ChristianLempa/boilerplates.git
synced 2026-08-24 10:04:44 -05:00
feat(cli): add optional variables and swarm volume configuration
- Add 'optional' property to variables allowing empty/None values - Implement swarm volume configuration with local, mount, and NFS backends - Fix Variable.to_dict() to preserve optional flag during serialization - Update traefik template with swarm configs and volume management - Add swarm volume variables to compose module spec
This commit is contained in:
@@ -23,6 +23,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- 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
|
||||
- Storage Configuration for Docker Compose
|
||||
- New `storage` section in compose module spec
|
||||
- New `config` section in compose module spec
|
||||
|
||||
+10
-1
@@ -539,6 +539,9 @@ class VariableCollection:
|
||||
|
||||
# Check required fields
|
||||
if variable.value is None:
|
||||
# Optional variables can be None/empty
|
||||
if hasattr(variable, 'optional') and variable.optional:
|
||||
continue
|
||||
if variable.is_required():
|
||||
errors.append(f"{section.key}.{var_name} (required - no default provided)")
|
||||
continue
|
||||
@@ -645,7 +648,7 @@ class VariableCollection:
|
||||
'prompt': other_var.prompt,
|
||||
'options': other_var.options,
|
||||
'sensitive': other_var.sensitive,
|
||||
'extra': other_var.extra
|
||||
'extra': other_var.extra,
|
||||
}
|
||||
|
||||
# Add fields that were explicitly provided, even if falsy/empty
|
||||
@@ -653,6 +656,12 @@ class VariableCollection:
|
||||
if field in other_var._explicit_fields:
|
||||
update[field] = value
|
||||
|
||||
# For boolean flags, only copy if explicitly provided in other
|
||||
# This prevents False defaults from overriding True values
|
||||
for bool_field in ('optional', 'autogenerated', 'required'):
|
||||
if bool_field in other_var._explicit_fields:
|
||||
update[bool_field] = getattr(other_var, bool_field)
|
||||
|
||||
# Special handling for value/default (allow explicit null to clear)
|
||||
if 'value' in other_var._explicit_fields:
|
||||
update['value'] = other_var.value
|
||||
|
||||
@@ -55,6 +55,8 @@ class Variable:
|
||||
self.autogenerated: bool = data.get("autogenerated", False)
|
||||
# Flag indicating this variable is required even when section is disabled
|
||||
self.required: bool = data.get("required", False)
|
||||
# Flag indicating this variable can be empty/optional
|
||||
self.optional: bool = data.get("optional", False)
|
||||
# Original value before config override (used for display)
|
||||
self.original_value: Optional[Any] = data.get("original_value")
|
||||
# Variable dependencies - can be string or list of strings in format "var_name=value"
|
||||
@@ -142,6 +144,10 @@ class Variable:
|
||||
if self.autogenerated and (converted is None or (isinstance(converted, str) and (converted == "" or converted == "*auto"))):
|
||||
return None # Signal that auto-generation should happen
|
||||
|
||||
# Allow empty values for optional variables
|
||||
if self.optional and (converted is None or (isinstance(converted, str) and converted == "")):
|
||||
return None
|
||||
|
||||
# Check if this is a required field and the value is empty
|
||||
if check_required and self.is_required():
|
||||
if converted is None or (isinstance(converted, str) and converted == ""):
|
||||
@@ -232,6 +238,8 @@ class Variable:
|
||||
result['autogenerated'] = True
|
||||
if self.required:
|
||||
result['required'] = True
|
||||
if self.optional:
|
||||
result['optional'] = True
|
||||
if self.options is not None: # Allow empty list
|
||||
result['options'] = self.options
|
||||
|
||||
@@ -339,11 +347,16 @@ class Variable:
|
||||
- It has an explicit 'required: true' flag (highest precedence)
|
||||
- OR it doesn't have a default value (value is None)
|
||||
AND it's not marked as autogenerated (which can be empty and generated later)
|
||||
AND it's not marked as optional (which can be empty)
|
||||
AND it's not a boolean type (booleans default to False if not set)
|
||||
|
||||
Returns:
|
||||
True if the variable must have a non-empty value, False otherwise
|
||||
"""
|
||||
# Optional variables can always be empty
|
||||
if self.optional:
|
||||
return False
|
||||
|
||||
# Explicit required flag takes highest precedence
|
||||
if self.required:
|
||||
# But autogenerated variables can still be empty (will be generated later)
|
||||
@@ -392,6 +405,7 @@ class Variable:
|
||||
'extra': self.extra,
|
||||
'autogenerated': self.autogenerated,
|
||||
'required': self.required,
|
||||
'optional': self.optional,
|
||||
'original_value': self.original_value,
|
||||
'needs': self.needs.copy() if self.needs else None,
|
||||
}
|
||||
|
||||
+40
-88
@@ -155,9 +155,46 @@ spec = OrderedDict(
|
||||
},
|
||||
"swarm_placement_host": {
|
||||
"description": "Target hostname for placement constraint",
|
||||
"type": "hostname",
|
||||
"type": "str",
|
||||
"default": "",
|
||||
"optional": True,
|
||||
"needs": "swarm_placement_mode=replicated",
|
||||
"extra": "Constrains service to run on specific node by hostname",
|
||||
"extra": "Constrains service to run on specific node by hostname (optional)",
|
||||
},
|
||||
"swarm_volume_mode": {
|
||||
"description": "Swarm volume storage backend",
|
||||
"type": "enum",
|
||||
"options": ["local", "mount", "nfs"],
|
||||
"default": "local",
|
||||
"extra": "WARNING: 'local' only works on single-node deployments!",
|
||||
},
|
||||
"swarm_volume_mount_path": {
|
||||
"description": "Host path for bind mount",
|
||||
"type": "str",
|
||||
"default": "/mnt/storage",
|
||||
"needs": "swarm_volume_mode=mount",
|
||||
"extra": "Useful for shared/replicated storage",
|
||||
},
|
||||
"swarm_volume_nfs_server": {
|
||||
"description": "NFS server address",
|
||||
"type": "str",
|
||||
"default": "192.168.1.1",
|
||||
"needs": "swarm_volume_mode=nfs",
|
||||
"extra": "IP address or hostname of NFS server",
|
||||
},
|
||||
"swarm_volume_nfs_path": {
|
||||
"description": "NFS export path",
|
||||
"type": "str",
|
||||
"default": "/export",
|
||||
"needs": "swarm_volume_mode=nfs",
|
||||
"extra": "Path to NFS export on the server",
|
||||
},
|
||||
"swarm_volume_nfs_options": {
|
||||
"description": "NFS mount options",
|
||||
"type": "str",
|
||||
"default": "rw,nolock,soft",
|
||||
"needs": "swarm_volume_mode=nfs",
|
||||
"extra": "Comma-separated NFS mount options",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -281,92 +318,7 @@ spec = OrderedDict(
|
||||
"sensitive": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
"storage": {
|
||||
"title": "Storage Configuration",
|
||||
"toggle": "storage_enabled",
|
||||
"description": "Configure persistent storage volumes",
|
||||
"vars": {
|
||||
"storage_enabled": {
|
||||
"description": "Enable storage configuration",
|
||||
"type": "bool",
|
||||
"default": False,
|
||||
},
|
||||
"storage_mode": {
|
||||
"description": "Storage backend",
|
||||
"type": "enum",
|
||||
"options": ["local", "mount", "nfs", "glusterfs"],
|
||||
"default": "local",
|
||||
"extra": "local=named volume, mount=bind mount, nfs=network filesystem, glusterfs=distributed storage",
|
||||
},
|
||||
"storage_host": {
|
||||
"description": "Storage host/volume identifier",
|
||||
"type": "str",
|
||||
"extra": "local: volume name, mount: host path, nfs: server IP, glusterfs: server hostname",
|
||||
},
|
||||
"storage_path": {
|
||||
"description": "NFS export path",
|
||||
"type": "str",
|
||||
"default": "/mnt/nfs",
|
||||
"extra": "Only used when storage_mode=nfs",
|
||||
},
|
||||
"storage_nfs_options": {
|
||||
"description": "NFS mount options",
|
||||
"type": "str",
|
||||
"default": "rw,nolock,soft",
|
||||
"extra": "Only used when storage_mode=nfs. Comma-separated options.",
|
||||
},
|
||||
"storage_glusterfs_volume": {
|
||||
"description": "GlusterFS volume name",
|
||||
"type": "str",
|
||||
"default": "gv0",
|
||||
"extra": "Only used when storage_mode=glusterfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
"config": {
|
||||
"title": "Config Storage",
|
||||
"toggle": "config_enabled",
|
||||
"description": "Configure persistent storage for configuration files",
|
||||
"vars": {
|
||||
"config_enabled": {
|
||||
"description": "Enable config storage configuration",
|
||||
"type": "bool",
|
||||
"default": False,
|
||||
},
|
||||
"config_mode": {
|
||||
"description": "Storage backend for configuration",
|
||||
"type": "enum",
|
||||
"options": ["local", "mount", "nfs", "glusterfs"],
|
||||
"default": "mount",
|
||||
"extra": "local=named volume, mount=bind mount, nfs=network filesystem, glusterfs=distributed storage",
|
||||
},
|
||||
"config_host": {
|
||||
"description": "Config storage host/volume identifier",
|
||||
"type": "str",
|
||||
"default": "./config",
|
||||
"extra": "local: volume name, mount: host path, nfs: server IP, glusterfs: server hostname",
|
||||
},
|
||||
"config_path": {
|
||||
"description": "NFS export path for config",
|
||||
"type": "str",
|
||||
"default": "/mnt/nfs/config",
|
||||
"extra": "Only used when config_mode=nfs",
|
||||
},
|
||||
"config_nfs_options": {
|
||||
"description": "NFS mount options for config",
|
||||
"type": "str",
|
||||
"default": "rw,nolock,soft",
|
||||
"extra": "Only used when config_mode=nfs. Comma-separated options.",
|
||||
},
|
||||
"config_glusterfs_volume": {
|
||||
"description": "GlusterFS volume name for config",
|
||||
"type": "str",
|
||||
"default": "gv0",
|
||||
"extra": "Only used when config_mode=glusterfs",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -14,8 +14,18 @@ services:
|
||||
{% endif %}
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
{% if not swarm_enabled %}
|
||||
- ./config/:/etc/traefik/:ro
|
||||
- ./certs/:/var/traefik/certs/:rw
|
||||
{% else %}
|
||||
{% if swarm_volume_mode == 'mount' %}
|
||||
- {{ swarm_volume_mount_path }}:/var/traefik/certs/:rw
|
||||
{% elif swarm_volume_mode == 'local' %}
|
||||
- traefik_certs:/var/traefik/certs/:rw
|
||||
{% elif swarm_volume_mode == 'nfs' %}
|
||||
- traefik_certs:/var/traefik/certs/:rw
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if traefik_tls_enabled %}
|
||||
{% if not swarm_enabled %}
|
||||
- ./.env.secret:/.env.secret:ro
|
||||
@@ -23,6 +33,17 @@ services:
|
||||
env_file:
|
||||
- ./.env
|
||||
{% endif %}
|
||||
{% if swarm_enabled %}
|
||||
configs:
|
||||
- source: traefik_config
|
||||
target: /etc/traefik/traefik.yaml
|
||||
- source: traefik_middlewares
|
||||
target: /etc/traefik/files/middlewares.yaml
|
||||
- source: traefik_tls
|
||||
target: /etc/traefik/files/tls.yaml
|
||||
- source: traefik_external_services
|
||||
target: /etc/traefik/files/external-services.yaml
|
||||
{% endif %}
|
||||
environment:
|
||||
- TZ={{ container_timezone }}
|
||||
healthcheck:
|
||||
@@ -43,27 +64,48 @@ services:
|
||||
mode: 0400
|
||||
{% endif %}
|
||||
deploy:
|
||||
{% if swarm_placement_mode in ['global', 'replicated'] %}
|
||||
mode: {{ swarm_placement_mode }}
|
||||
{% if swarm_placement_mode == 'replicated' %}
|
||||
replicas: {{ swarm_replicas }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
mode: replicated
|
||||
replicas: {{ swarm_replicas }}
|
||||
{% if swarm_placement_host %}
|
||||
placement:
|
||||
constraints:
|
||||
- {{ swarm_placement_mode }}
|
||||
- node.hostname == {{ swarm_placement_host }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
restart: {{ restart_policy }}
|
||||
{% endif %}
|
||||
|
||||
{% if swarm_enabled and traefik_tls_enabled %}
|
||||
{% if swarm_enabled %}
|
||||
{% if swarm_volume_mode in ['local', 'nfs'] %}
|
||||
volumes:
|
||||
traefik_certs:
|
||||
{% if swarm_volume_mode == 'nfs' %}
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: nfs
|
||||
o: addr={{ swarm_volume_nfs_server }},{{ swarm_volume_nfs_options }}
|
||||
device: ":{{ swarm_volume_nfs_path }}"
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
configs:
|
||||
traefik_config:
|
||||
file: ./config/traefik.yaml
|
||||
traefik_middlewares:
|
||||
file: ./config/files/middlewares.yaml
|
||||
traefik_tls:
|
||||
file: ./config/files/tls.yaml
|
||||
traefik_external_services:
|
||||
file: ./config/files/external-services.yaml
|
||||
|
||||
{% if traefik_tls_enabled %}
|
||||
secrets:
|
||||
{{ traefik_tls_acme_secret_name }}:
|
||||
file: ./.env.secret
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if network_enabled %}
|
||||
networks:
|
||||
@@ -71,6 +113,12 @@ networks:
|
||||
{% if network_external %}
|
||||
external: true
|
||||
{% else %}
|
||||
{% if swarm_enabled %}
|
||||
driver: overlay
|
||||
attachable: true
|
||||
{% else %}
|
||||
driver: bridge
|
||||
{% endif %}
|
||||
name: {{ network_name }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -139,10 +139,6 @@ spec:
|
||||
default: true
|
||||
network_name:
|
||||
default: "proxy"
|
||||
swarm:
|
||||
vars:
|
||||
swarm_placement_mode:
|
||||
default: "global"
|
||||
authentik:
|
||||
title: Authentik Middleware
|
||||
description: Enable Authentik SSO integration for Traefik
|
||||
|
||||
Reference in New Issue
Block a user