ignition-stack 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/PKG-INFO +1 -1
  2. ignition_stack-0.3.0/builtin_modules.yaml +122 -0
  3. ignition_stack-0.3.0/ignition_stack/__init__.py +1 -0
  4. ignition_stack-0.3.0/ignition_stack/catalog/builtins.py +171 -0
  5. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/cli.py +25 -1
  6. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/completion.py +15 -0
  7. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/compose/engine.py +29 -0
  8. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/compose/templates/services/ignition.yaml.j2 +3 -0
  9. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/config/schema.py +29 -2
  10. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/profiles/base.py +34 -1
  11. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/services/resolver.py +1 -0
  12. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/wizard.py +43 -2
  13. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/pyproject.toml +2 -1
  14. ignition_stack-0.3.0/tests/test_builtin_catalog_smoke.py +114 -0
  15. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_compose_engine.py +3 -4
  16. ignition_stack-0.3.0/tests/test_disable_builtins.py +249 -0
  17. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_profiles.py +58 -0
  18. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_service_catalog.py +16 -8
  19. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_service_catalog_smoke.py +3 -3
  20. ignition_stack-0.2.0/ignition_stack/__init__.py +0 -1
  21. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/.gitignore +0 -0
  22. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/LICENSE +0 -0
  23. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/README.md +0 -0
  24. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/catalog/__init__.py +0 -0
  25. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/catalog/download.py +0 -0
  26. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/catalog/loader.py +0 -0
  27. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/catalog/schema.py +0 -0
  28. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/catalog/verify.py +0 -0
  29. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/commands/__init__.py +0 -0
  30. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/commands/modules.py +0 -0
  31. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/compose/__init__.py +0 -0
  32. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/compose/templates/footer.yaml.j2 +0 -0
  33. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/compose/templates/header.yaml.j2 +0 -0
  34. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/compose/templates/services/bootstrap.yaml.j2 +0 -0
  35. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/compose/writer.py +0 -0
  36. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/config/__init__.py +0 -0
  37. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/config/io.py +0 -0
  38. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/lifecycle/__init__.py +0 -0
  39. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/lifecycle/cleanup.py +0 -0
  40. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/lifecycle/record.py +0 -0
  41. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/lifecycle/regenerate.py +0 -0
  42. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/postsetup/__init__.py +0 -0
  43. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/postsetup/generator.py +0 -0
  44. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/profiles/__init__.py +0 -0
  45. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/profiles/advisory.py +0 -0
  46. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/profiles/hub_and_spoke.py +0 -0
  47. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/profiles/mcp_n8n.py +0 -0
  48. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/profiles/scaleout.py +0 -0
  49. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/profiles/standalone.py +0 -0
  50. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/services/__init__.py +0 -0
  51. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/services/loader.py +0 -0
  52. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/services/manifest.py +0 -0
  53. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/__init__.py +0 -0
  54. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/post-setup/_default.md.j2 +0 -0
  55. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/post-setup/device-connection.md.j2 +0 -0
  56. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/post-setup/gateway-network-link.md.j2 +0 -0
  57. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/post-setup/identity-provider.md.j2 +0 -0
  58. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/post-setup/kafka-connector.md.j2 +0 -0
  59. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/post-setup/mcp-module.md.j2 +0 -0
  60. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/post-setup/mqtt-engine-connection.md.j2 +0 -0
  61. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/post-setup/opc-ua-connection.md.j2 +0 -0
  62. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/post-setup/redundancy-pairing.md.j2 +0 -0
  63. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/post-setup/reverse-proxy.md.j2 +0 -0
  64. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/redundancy/redundancy.xml.j2 +0 -0
  65. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/chariot/compose.yaml.j2 +0 -0
  66. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/chariot/manifest.yaml +0 -0
  67. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/chariot/seed/service/USAGE.md +0 -0
  68. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/emqx/compose.yaml.j2 +0 -0
  69. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/emqx/manifest.yaml +0 -0
  70. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/emqx/seed/service/USAGE.md +0 -0
  71. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/hivemq/compose.yaml.j2 +0 -0
  72. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/hivemq/manifest.yaml +0 -0
  73. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/hivemq/seed/service/USAGE.md +0 -0
  74. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/kafka/compose.yaml.j2 +0 -0
  75. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/kafka/manifest.yaml +0 -0
  76. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/kafka/seed/service/USAGE.md +0 -0
  77. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/keycloak/compose.yaml.j2 +0 -0
  78. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/keycloak/manifest.yaml +0 -0
  79. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/keycloak/seed/service/import/ignition-realm.json +0 -0
  80. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/mariadb/compose.yaml.j2 +0 -0
  81. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/mariadb/manifest.yaml +0 -0
  82. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/mariadb/seed/service/initdb/00-create-extra-databases.sh +0 -0
  83. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/modbus-sim/compose.yaml.j2 +0 -0
  84. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/modbus-sim/manifest.yaml +0 -0
  85. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/modbus-sim/seed/service/USAGE.md +0 -0
  86. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/mongo/compose.yaml.j2 +0 -0
  87. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/mongo/manifest.yaml +0 -0
  88. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/mongo/seed/service/initdb/01-demo-collection.js +0 -0
  89. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/mysql/compose.yaml.j2 +0 -0
  90. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/mysql/manifest.yaml +0 -0
  91. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/mysql/seed/service/initdb/00-create-extra-databases.sh +0 -0
  92. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/n8n/compose.yaml.j2 +0 -0
  93. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/n8n/manifest.yaml +0 -0
  94. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/n8n/seed/service/USAGE.md +0 -0
  95. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/opcua-sim/compose.yaml.j2 +0 -0
  96. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/opcua-sim/manifest.yaml +0 -0
  97. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/opcua-sim/seed/service/USAGE.md +0 -0
  98. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/postgres/compose.yaml.j2 +0 -0
  99. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/postgres/manifest.yaml +0 -0
  100. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/config.json +0 -0
  101. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/resource.json +0 -0
  102. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/secret-provider/internal-secret-provider/config.json +0 -0
  103. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/secret-provider/internal-secret-provider/resource.json +0 -0
  104. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/postgres/seed/service/initdb/00-create-extra-databases.sh +0 -0
  105. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/rabbitmq/compose.yaml.j2 +0 -0
  106. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/rabbitmq/manifest.yaml +0 -0
  107. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/services/rabbitmq/seed/service/enabled_plugins +0 -0
  108. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/standalone-postgres/docker-compose.yaml +0 -0
  109. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/standalone-postgres/scripts/docker-bootstrap.sh +0 -0
  110. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/standalone-postgres/services/ignition/config/resources/core/config-mode.json +0 -0
  111. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/standalone-postgres/services/ignition/config/resources/dev/config-mode.json +0 -0
  112. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/ignition_stack/templates/standalone-postgres/services/ignition/projects/.gitkeep +0 -0
  113. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/modules.yaml +0 -0
  114. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/__init__.py +0 -0
  115. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/conftest.py +0 -0
  116. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/combos/network-split/docker-compose.yaml +0 -0
  117. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/combos/smoke-stack/docker-compose.yaml +0 -0
  118. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/profiles/hub-and-spoke/docker-compose.yaml +0 -0
  119. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/profiles/mcp-n8n/docker-compose.yaml +0 -0
  120. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/profiles/scaleout/docker-compose.yaml +0 -0
  121. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/profiles/scaleout-redundant/docker-compose.yaml +0 -0
  122. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/profiles/standalone/docker-compose.yaml +0 -0
  123. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/scaleout-skeleton/docker-compose.yaml +0 -0
  124. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/chariot/docker-compose.yaml +0 -0
  125. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/db-mariadb/docker-compose.yaml +0 -0
  126. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/db-mongo/docker-compose.yaml +0 -0
  127. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/db-mysql/docker-compose.yaml +0 -0
  128. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/db-postgres/docker-compose.yaml +0 -0
  129. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/emqx/docker-compose.yaml +0 -0
  130. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/hivemq/docker-compose.yaml +0 -0
  131. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/kafka/docker-compose.yaml +0 -0
  132. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/keycloak/docker-compose.yaml +0 -0
  133. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/modbus-sim/docker-compose.yaml +0 -0
  134. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/n8n/docker-compose.yaml +0 -0
  135. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/opcua-sim/docker-compose.yaml +0 -0
  136. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/services/rabbitmq/docker-compose.yaml +0 -0
  137. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/golden/standalone-postgres/docker-compose.yaml +0 -0
  138. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_completion.py +0 -0
  139. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_declarative_io.py +0 -0
  140. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_docs_cli_reference.py +0 -0
  141. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_init_standalone.py +0 -0
  142. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_lifecycle.py +0 -0
  143. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_modules_catalog.py +0 -0
  144. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_modules_cli.py +0 -0
  145. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_postsetup.py +0 -0
  146. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/tests/test_redundancy.py +0 -0
  147. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/verification/redundancy-spike/README.md +0 -0
  148. {ignition_stack-0.2.0 → ignition_stack-0.3.0}/verification/smoke/README.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ignition-stack
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: CLI that generates ready-to-run Docker Compose stacks for Ignition 8.3 SCADA demos and SE engagements
5
5
  Author-email: Eric Knorr <etknorr@gmail.com>
6
6
  License: MIT
@@ -0,0 +1,122 @@
1
+ # Catalog of the built-in IA modules that ship inside the Ignition gateway
2
+ # image. Used to translate a gateway's `disable_builtins` slugs into the
3
+ # GATEWAY_MODULES_ENABLED whitelist the engine emits (enabled = all built-ins
4
+ # minus the disabled ones, plus any third-party module identifiers).
5
+ #
6
+ # Why this file exists:
7
+ # GATEWAY_MODULES_ENABLED is a strict WHITELIST, not a blocklist - anything
8
+ # not listed is quarantined at boot (verified live on 8.3.6). So "disable
9
+ # Vision" can only be expressed as "enable every built-in except Vision".
10
+ # That inversion needs the COMPLETE built-in set; an incomplete list would
11
+ # silently quarantine the modules we forgot to enumerate. This file is that
12
+ # complete set, and tests/test_builtin_catalog.py (marked `smoke`) re-derives
13
+ # it from the live image so a stale list fails CI loudly instead of dropping
14
+ # modules silently.
15
+ #
16
+ # Provenance (how to regenerate when bumping the Ignition image):
17
+ # 1. Boot the pinned image with no whitelist so every built-in loads:
18
+ # docker run --rm -e ACCEPT_IGNITION_EULA=Y -e GATEWAY_ADMIN_PASSWORD=x \
19
+ # inductiveautomation/ignition:<tag>
20
+ # 2. Read the loaded set from the gateway log lines:
21
+ # Starting up module '<identifier>' ... module-name=<name>
22
+ # 3. Update `ignition_version` and the `modules` list below to match.
23
+ # The smoke guard test automates this comparison.
24
+ #
25
+ # `identifier` is the fully-qualified module id used verbatim in
26
+ # GATEWAY_MODULES_ENABLED. `slug` is the friendly kebab name a user puts in
27
+ # `disable_builtins`. `name` is the gateway's display name (for wizard labels).
28
+
29
+ version: 1
30
+
31
+ # The exact Ignition image tag this built-in set was captured from. The guard
32
+ # test only asserts a match when the running image reports this version.
33
+ ignition_version: "8.3.6"
34
+
35
+ modules:
36
+ - slug: alarm-notification
37
+ identifier: com.inductiveautomation.alarm-notification
38
+ name: Alarm Notification
39
+ - slug: allen-bradley-driver
40
+ identifier: com.inductiveautomation.opcua.drivers.ablegacy
41
+ name: Allen-Bradley Driver
42
+ - slug: bacnet-driver
43
+ identifier: com.inductiveautomation.opcua.drivers.bacnet
44
+ name: BACnet Driver
45
+ - slug: enterprise-administration
46
+ identifier: com.inductiveautomation.eam
47
+ name: Enterprise Administration
48
+ - slug: event-streams
49
+ identifier: com.inductiveautomation.eventstream
50
+ name: Event Streams
51
+ - slug: historian-core
52
+ identifier: com.inductiveautomation.historian
53
+ name: Historian Core
54
+ - slug: kafka-connector
55
+ identifier: com.inductiveautomation.connectors.kafka
56
+ name: Kafka Connector
57
+ - slug: legacy-dnp3-driver
58
+ identifier: com.inductiveautomation.opcua.drivers.dnp3
59
+ name: Legacy DNP3 Driver
60
+ - slug: logix-driver
61
+ identifier: com.inductiveautomation.opcua.drivers.logix
62
+ name: Logix Driver
63
+ - slug: mariadb-jdbc-driver
64
+ identifier: com.inductiveautomation.jdbc.mariadb
65
+ name: MariaDB JDBC Driver
66
+ - slug: micro800-driver
67
+ identifier: com.inductiveautomation.opcua.drivers.micro800
68
+ name: Micro800 Driver
69
+ - slug: mitsubishi-driver
70
+ identifier: com.inductiveautomation.opcua.drivers.mitsubishi
71
+ name: Mitsubishi Driver
72
+ - slug: modbus-driver
73
+ identifier: com.inductiveautomation.opcua.drivers.modbus
74
+ name: Modbus Driver
75
+ - slug: mssql-jdbc-driver
76
+ identifier: com.inductiveautomation.jdbc.mssql
77
+ name: MSSQL JDBC Driver
78
+ - slug: omron-driver
79
+ identifier: com.inductiveautomation.opcua.drivers.omron
80
+ name: Omron Driver
81
+ - slug: opc-ua
82
+ identifier: com.inductiveautomation.opcua
83
+ name: OPC-UA
84
+ - slug: perspective
85
+ identifier: com.inductiveautomation.perspective
86
+ name: Perspective
87
+ - slug: postgresql-jdbc-driver
88
+ identifier: com.inductiveautomation.jdbc.postgresql
89
+ name: PostgreSQL JDBC Driver
90
+ - slug: reporting
91
+ identifier: com.inductiveautomation.reporting
92
+ name: Reporting
93
+ - slug: sfc
94
+ identifier: com.inductiveautomation.sfc
95
+ name: SFC
96
+ - slug: siemens-drivers
97
+ identifier: com.inductiveautomation.opcua.drivers.siemens
98
+ name: Siemens Drivers
99
+ - slug: siemens-enhanced-driver
100
+ identifier: com.inductiveautomation.opcua.drivers.siemens-symbolic
101
+ name: Siemens Enhanced Driver
102
+ - slug: sms-notification
103
+ identifier: com.inductiveautomation.sms-notification
104
+ name: SMS Notification
105
+ - slug: sql-bridge
106
+ identifier: com.inductiveautomation.sqlbridge
107
+ name: SQL Bridge
108
+ - slug: sql-historian
109
+ identifier: com.inductiveautomation.historian.sql
110
+ name: SQL Historian
111
+ - slug: symbol-factory
112
+ identifier: com.inductiveautomation.symbol-factory
113
+ name: Symbol Factory
114
+ - slug: udp-and-tcp-drivers
115
+ identifier: com.inductiveautomation.opcua.drivers.tcpudp
116
+ name: UDP and TCP Drivers
117
+ - slug: vision
118
+ identifier: com.inductiveautomation.vision
119
+ name: Vision
120
+ - slug: webdev
121
+ identifier: com.inductiveautomation.webdev
122
+ name: WebDev
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -0,0 +1,171 @@
1
+ """Load and query the built-in IA module catalog (``builtin_modules.yaml``).
2
+
3
+ The third-party catalog (``modules.yaml`` + ``catalog/schema.py``) covers
4
+ modules the CLI *adds*. This module covers the modules that *already ship*
5
+ inside the gateway image, which the engine needs in order to translate a
6
+ gateway's ``disable_builtins`` slugs into a ``GATEWAY_MODULES_ENABLED``
7
+ whitelist.
8
+
9
+ The whitelist is strict: anything not listed is quarantined at boot. So
10
+ "disable Vision" is expressed as "enable every built-in except Vision",
11
+ which requires the *complete* built-in set - hence a pinned data file plus a
12
+ smoke guard test that re-derives it from the live image.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from functools import lru_cache
18
+ from importlib import resources
19
+ from pathlib import Path
20
+ from typing import Annotated
21
+
22
+ import yaml
23
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
24
+
25
+
26
+ class BuiltinCatalogLoadError(Exception):
27
+ """Raised when builtin_modules.yaml cannot be read or fails validation."""
28
+
29
+
30
+ DEFAULT_BUILTIN_CATALOG_NAME = "builtin_modules.yaml"
31
+
32
+
33
+ class BuiltinModule(BaseModel):
34
+ """One built-in IA module that ships inside the gateway image."""
35
+
36
+ model_config = ConfigDict(extra="forbid", frozen=True)
37
+
38
+ slug: Annotated[
39
+ str,
40
+ Field(
41
+ min_length=1,
42
+ pattern=r"^[a-z0-9][a-z0-9-]*$",
43
+ description="Friendly kebab name a user puts in `disable_builtins`.",
44
+ ),
45
+ ]
46
+ identifier: Annotated[
47
+ str,
48
+ Field(
49
+ min_length=1,
50
+ pattern=r"^[a-z0-9.-]+$",
51
+ description="Fully-qualified module id, used verbatim in GATEWAY_MODULES_ENABLED.",
52
+ ),
53
+ ]
54
+ name: Annotated[str, Field(min_length=1, description="Gateway display name (wizard label).")]
55
+
56
+
57
+ class BuiltinCatalog(BaseModel):
58
+ """Top-level shape of builtin_modules.yaml."""
59
+
60
+ model_config = ConfigDict(extra="forbid", frozen=True)
61
+
62
+ version: Annotated[int, Field(ge=1)]
63
+ ignition_version: Annotated[
64
+ str,
65
+ Field(min_length=1, description="Image tag this built-in set was captured from."),
66
+ ]
67
+ modules: Annotated[list[BuiltinModule], Field(min_length=1)]
68
+
69
+ @field_validator("modules")
70
+ @classmethod
71
+ def _slugs_unique(cls, modules: list[BuiltinModule]) -> list[BuiltinModule]:
72
+ slugs = [m.slug for m in modules]
73
+ dupes = sorted({s for s in slugs if slugs.count(s) > 1})
74
+ if dupes:
75
+ raise ValueError(f"duplicate built-in slugs: {', '.join(dupes)}")
76
+ return modules
77
+
78
+ @property
79
+ def slugs(self) -> set[str]:
80
+ """Every known built-in slug."""
81
+ return {m.slug for m in self.modules}
82
+
83
+ def identifiers_excluding(self, disabled_slugs: list[str]) -> list[str]:
84
+ """FQ identifiers of every built-in whose slug is not in ``disabled_slugs``.
85
+
86
+ Order follows the catalog (already alphabetical by slug) so generated
87
+ whitelists are deterministic and golden-stable.
88
+ """
89
+ disabled = set(disabled_slugs)
90
+ return [m.identifier for m in self.modules if m.slug not in disabled]
91
+
92
+
93
+ def load_builtin_catalog(path: Path | None = None) -> BuiltinCatalog:
94
+ """Load and validate the built-in catalog.
95
+
96
+ With ``path=None`` the catalog shipped with the installed package is used;
97
+ a path overrides it (test fixtures). Mirrors ``catalog.loader.load_catalog``.
98
+ """
99
+ yaml_text = _read_yaml_text(path)
100
+ name = DEFAULT_BUILTIN_CATALOG_NAME
101
+ try:
102
+ raw = yaml.safe_load(yaml_text)
103
+ except yaml.YAMLError as exc:
104
+ raise BuiltinCatalogLoadError(f"{name} is not valid YAML: {exc}") from exc
105
+
106
+ if not isinstance(raw, dict):
107
+ raise BuiltinCatalogLoadError(f"{name} top-level must be a mapping.")
108
+
109
+ try:
110
+ return BuiltinCatalog.model_validate(raw)
111
+ except ValidationError as exc:
112
+ raise BuiltinCatalogLoadError(
113
+ f"{DEFAULT_BUILTIN_CATALOG_NAME} failed schema validation:\n{exc}"
114
+ ) from exc
115
+
116
+
117
+ @lru_cache(maxsize=1)
118
+ def default_builtin_catalog() -> BuiltinCatalog:
119
+ """The built-in catalog shipped with the package, loaded once and cached.
120
+
121
+ The data file is immutable package data, so both config validation and the
122
+ compose engine can share a single memoized read rather than re-parsing YAML
123
+ on every gateway.
124
+ """
125
+ return load_builtin_catalog()
126
+
127
+
128
+ def builtin_slugs() -> frozenset[str]:
129
+ """Slugs of the shipped built-in catalog, for cheap ``disable_builtins`` validation."""
130
+ return frozenset(default_builtin_catalog().slugs)
131
+
132
+
133
+ def validate_disable_slugs(slugs: list[str]) -> None:
134
+ """Raise ``ValueError`` if any slug is not a known built-in.
135
+
136
+ Shared by ``GatewayConfig`` field validation (construction-time) and
137
+ ``profiles.apply_disable_builtins`` (post-construction mutation, which
138
+ pydantic does not re-validate), so the wizard/CLI path is guarded too. A
139
+ typo would otherwise be a silent no-op - the slug just isn't disabled.
140
+ """
141
+ known = builtin_slugs()
142
+ unknown = [s for s in slugs if s not in known]
143
+ if unknown:
144
+ raise ValueError(
145
+ f"unknown built-in module slug(s): {', '.join(unknown)}. "
146
+ f"Valid slugs are: {', '.join(sorted(known))}"
147
+ )
148
+
149
+
150
+ def _read_yaml_text(path: Path | None) -> str:
151
+ if path is not None:
152
+ if not path.is_file():
153
+ raise BuiltinCatalogLoadError(f"Built-in catalog not found at {path}.")
154
+ return path.read_text(encoding="utf-8")
155
+
156
+ # Installed wheels: force-included as ignition_stack/builtin_modules.yaml.
157
+ # Editable dev installs: it lives at the repo root next to pyproject.toml.
158
+ try:
159
+ bundled = resources.files("ignition_stack").joinpath(DEFAULT_BUILTIN_CATALOG_NAME)
160
+ if bundled.is_file():
161
+ return bundled.read_text(encoding="utf-8")
162
+ except (FileNotFoundError, OSError, ModuleNotFoundError):
163
+ pass
164
+
165
+ repo_root = Path(__file__).resolve().parents[2]
166
+ dev_path = repo_root / DEFAULT_BUILTIN_CATALOG_NAME
167
+ if not dev_path.is_file():
168
+ raise BuiltinCatalogLoadError(
169
+ f"{DEFAULT_BUILTIN_CATALOG_NAME} not found in package data or at {dev_path}."
170
+ )
171
+ return dev_path.read_text(encoding="utf-8")
@@ -22,6 +22,7 @@ from rich.console import Console
22
22
  from ignition_stack import __version__
23
23
  from ignition_stack.commands.modules import modules_app
24
24
  from ignition_stack.completion import (
25
+ complete_disable_builtin,
25
26
  complete_edge_role,
26
27
  complete_output_format,
27
28
  complete_profile,
@@ -172,6 +173,17 @@ def init(
172
173
  ),
173
174
  autocompletion=complete_redundant_role,
174
175
  ),
176
+ disable_builtin: list[str] = typer.Option( # noqa: B008 - Typer pattern
177
+ [],
178
+ "--disable-builtin",
179
+ help=(
180
+ "Built-in IA module to turn off on every gateway (repeatable), e.g. "
181
+ "--disable-builtin vision --disable-builtin sfc. Emits a "
182
+ "GATEWAY_MODULES_ENABLED whitelist of everything else. Slugs "
183
+ "tab-complete; an unknown slug is rejected with the full valid list."
184
+ ),
185
+ autocompletion=complete_disable_builtin,
186
+ ),
175
187
  from_file: Path | None = typer.Option( # noqa: B008 - Typer pattern
176
188
  None,
177
189
  "--from-file",
@@ -240,6 +252,7 @@ def init(
240
252
  reverse_proxy=reverse_proxy,
241
253
  proxy_path=proxy_path,
242
254
  redundant=redundant,
255
+ disable_builtin=disable_builtin,
243
256
  )
244
257
 
245
258
  if dry_run:
@@ -328,6 +341,7 @@ def _build_from_profile(
328
341
  reverse_proxy: str | None,
329
342
  proxy_path: str,
330
343
  redundant: str | None,
344
+ disable_builtin: list[str],
331
345
  ) -> ProjectConfig:
332
346
  """Materialize a config from the named profile + CLI flags, or exit cleanly."""
333
347
  try:
@@ -345,6 +359,7 @@ def _build_from_profile(
345
359
  network_split=network_split,
346
360
  reverse_proxy=proxy,
347
361
  redundant_role=redundant,
362
+ disable_builtins=tuple(disable_builtin),
348
363
  )
349
364
  try:
350
365
  config = build_profile(profile, name, options)
@@ -463,7 +478,9 @@ def _options_from_config(config: ProjectConfig) -> ProfileOptions:
463
478
  'none' to keep the new profile from re-introducing its edge default); the
464
479
  spoke count from the number of spoke-role gateways, the frontend count from
465
480
  the number of frontend-role gateways, and the network split is carried over
466
- verbatim so a reshape preserves the user's topology choice.
481
+ verbatim so a reshape preserves the user's topology choice. Disabled
482
+ built-in modules are carried over too (see below) so a reshape doesn't
483
+ silently re-enable Vision/SFC/etc.
467
484
  """
468
485
  edge_roles = [gw.role or gw.name for gw in config.gateways if gw.ignition_edition == "edge"]
469
486
  spoke_count = sum(1 for gw in config.gateways if (gw.role or "") == "spoke")
@@ -479,6 +496,12 @@ def _options_from_config(config: ProjectConfig) -> ProfileOptions:
479
496
  ),
480
497
  None,
481
498
  )
499
+ # Disabled built-ins are applied stack-wide, so carry over the slugs disabled
500
+ # on EVERY gateway (the intersection) - that is the stack-wide intent, and it
501
+ # won't over-disable a module that a hand-authored config turned off on only
502
+ # one node. The target profile re-applies it uniformly.
503
+ disabled_sets = [set(gw.disable_builtins) for gw in config.gateways]
504
+ disable_builtins = tuple(sorted(set.intersection(*disabled_sets))) if disabled_sets else ()
482
505
  return ProfileOptions(
483
506
  spokes=spoke_count or 3,
484
507
  frontends=frontend_count or 1,
@@ -488,6 +511,7 @@ def _options_from_config(config: ProjectConfig) -> ProfileOptions:
488
511
  database_kind=config.database.kind if config.database else None,
489
512
  services=tuple(config.services),
490
513
  redundant_role=redundant_role,
514
+ disable_builtins=disable_builtins,
491
515
  )
492
516
 
493
517
 
@@ -77,3 +77,18 @@ def complete_module_name(incomplete: str) -> list[str]:
77
77
  # catalog must degrade to "no suggestions", never break the shell line.
78
78
  return []
79
79
  return [entry.name for entry in entries if entry.name.startswith(incomplete)]
80
+
81
+
82
+ def complete_disable_builtin(incomplete: str) -> list[tuple[str, str]]:
83
+ """Built-in module slugs (with display name) matching the typed prefix.
84
+
85
+ Reads the bundled built-in catalog; degrades to no suggestions on any error
86
+ so a TAB never breaks the shell line.
87
+ """
88
+ try:
89
+ from ignition_stack.catalog.builtins import default_builtin_catalog
90
+
91
+ modules = default_builtin_catalog().modules
92
+ except Exception:
93
+ return []
94
+ return [(m.slug, m.name) for m in modules if m.slug.startswith(incomplete)]
@@ -39,6 +39,7 @@ from typing import TYPE_CHECKING
39
39
  from jinja2 import Environment, PackageLoader, StrictUndefined
40
40
  from ruamel.yaml import YAML
41
41
 
42
+ from ignition_stack.catalog.builtins import default_builtin_catalog
42
43
  from ignition_stack.services.loader import load_all_services, load_service
43
44
 
44
45
  if TYPE_CHECKING:
@@ -280,6 +281,11 @@ def _ignition_context(
280
281
  "memory_mb": gw.memory_mb,
281
282
  "edition_override": edition_override,
282
283
  "module_identifiers": ctx["module_identifiers"],
284
+ # disable_active drives template emission (not the value's truthiness) so
285
+ # that disabling EVERY built-in emits an empty whitelist - which quarantines
286
+ # all, matching intent - instead of omitting the var and re-enabling all.
287
+ "disable_active": bool(gw.disable_builtins),
288
+ "modules_enabled": _modules_enabled_for(gw, ctx["module_identifiers"]), # type: ignore[arg-type]
283
289
  "database_service": config.database.name if config.database else None,
284
290
  "networks": ctx["networks"],
285
291
  "redundant": is_redundant,
@@ -314,6 +320,29 @@ def _module_identifiers_for(gw: GatewayConfig, catalog: Catalog | None) -> str:
314
320
  return ",".join(identifiers)
315
321
 
316
322
 
323
+ def _modules_enabled_for(gw: GatewayConfig, module_identifiers: str) -> str:
324
+ """The GATEWAY_MODULES_ENABLED whitelist VALUE for this gateway.
325
+
326
+ GATEWAY_MODULES_ENABLED is a strict whitelist: if set, every built-in not
327
+ listed is quarantined at boot. The template emits the var based on
328
+ ``disable_active`` (whether any built-in was disabled), not on this value's
329
+ truthiness - so disabling every built-in yields an empty string here and an
330
+ empty whitelist downstream (quarantines all), rather than silently omitting
331
+ the var and re-enabling everything.
332
+
333
+ When something is disabled the whitelist must be complete: every built-in the
334
+ user did not disable, PLUS any third-party modules we added
335
+ (``module_identifiers``) - or those added modules would be quarantined too.
336
+ Returns '' when nothing is disabled (the var is omitted in that case).
337
+ """
338
+ if not gw.disable_builtins:
339
+ return ""
340
+ enabled = default_builtin_catalog().identifiers_excluding(gw.disable_builtins)
341
+ added = [ident for ident in module_identifiers.split(",") if ident]
342
+ enabled.extend(ident for ident in added if ident not in enabled)
343
+ return ",".join(enabled)
344
+
345
+
317
346
  def _is_module(entry: ModuleEntry | object) -> bool:
318
347
  return getattr(entry, "kind", None) == "module"
319
348
 
@@ -22,6 +22,9 @@
22
22
  ACCEPT_MODULE_LICENSES: "{{ module_identifiers }}"
23
23
  ACCEPT_MODULE_CERTS: "{{ module_identifiers }}"
24
24
  {%- endif %}
25
+ {%- if disable_active %}
26
+ GATEWAY_MODULES_ENABLED: "{{ modules_enabled }}"
27
+ {%- endif %}
25
28
  {%- if redundant %}
26
29
  GATEWAY_NETWORK_ENABLED: "true"
27
30
  GATEWAY_NETWORK_ALLOWINCOMING: "true"
@@ -124,8 +124,22 @@ class GatewayConfig(BaseModel):
124
124
  "Module catalog entry names (slugs) to attach to this gateway. "
125
125
  "The compose engine wires each cached .modl into the gateway "
126
126
  "volume AND enumerates it in ACCEPT_MODULE_LICENSES + "
127
- "ACCEPT_MODULE_CERTS per the resolved q-module-install finding "
128
- "(GATEWAY_MODULES_ENABLED is omitted - it quarantines built-ins)."
127
+ "ACCEPT_MODULE_CERTS per the resolved q-module-install finding. "
128
+ "These added identifiers are also folded into the "
129
+ "GATEWAY_MODULES_ENABLED whitelist when disable_builtins is set, "
130
+ "so disabling a built-in never quarantines an added module."
131
+ ),
132
+ )
133
+ disable_builtins: list[str] = Field(
134
+ default_factory=list,
135
+ description=(
136
+ "Slugs of built-in IA modules to turn off on this gateway, e.g. "
137
+ "['vision', 'sfc']. Because the gateway's GATEWAY_MODULES_ENABLED "
138
+ "env var is a strict whitelist (anything unlisted is quarantined), "
139
+ "the engine inverts this into 'enable every built-in except these, "
140
+ "plus any added modules'. Empty (default) emits no whitelist and "
141
+ "leaves all built-ins on - the historical behavior. Slugs are "
142
+ "validated against builtin_modules.yaml; an unknown slug raises."
129
143
  ),
130
144
  )
131
145
  redundancy: RedundancyConfig | None = Field(
@@ -155,6 +169,19 @@ class GatewayConfig(BaseModel):
155
169
  raise ValueError("ignition_edition must be 'standard' or 'edge'")
156
170
  return v
157
171
 
172
+ @field_validator("disable_builtins")
173
+ @classmethod
174
+ def _validate_disable_builtins(cls, v: list[str]) -> list[str]:
175
+ # Reject unknown slugs loudly: a typo here would otherwise be a silent
176
+ # no-op (the slug isn't in the catalog, so nothing gets disabled),
177
+ # which is exactly the surprise the whitelist inversion must avoid.
178
+ # Imported locally to keep the config schema free of a load-time catalog
179
+ # dependency (and dodge any import-order coupling).
180
+ from ignition_stack.catalog.builtins import validate_disable_slugs
181
+
182
+ validate_disable_slugs(v)
183
+ return v
184
+
158
185
  @property
159
186
  def env_prefix(self) -> str:
160
187
  """Uppercase prefix for this gateway's per-gateway env-var keys.
@@ -86,6 +86,14 @@ class ProfileOptions:
86
86
  workhorse role (scaleout 'backend', hub-and-spoke 'hub', standalone
87
87
  'gateway'); replicated tiers ('frontend', 'spoke') are rejected."""
88
88
 
89
+ disable_builtins: tuple[str, ...] = ()
90
+ """Built-in module slugs to turn off on every gateway in the stack.
91
+
92
+ Empty (default) leaves all built-ins on. Applied uniformly by
93
+ ``build_profile`` - the demo intent is "drop Vision/SFC everywhere", and
94
+ per-gateway disabling stays a declarative-config-only feature. Slugs are
95
+ validated against builtin_modules.yaml by ``GatewayConfig``."""
96
+
89
97
 
90
98
  class Profile(Protocol):
91
99
  """A factory that turns ``ProfileOptions`` into a ``ProjectConfig``."""
@@ -200,4 +208,29 @@ def build_profile(slug: str, name: str, options: ProfileOptions) -> ProjectConfi
200
208
  means one eligibility rule serves every profile and the wizard alike.
201
209
  """
202
210
  config = get_profile(slug).build(name, options)
203
- return mark_redundant(config, options.redundant_role)
211
+ config = mark_redundant(config, options.redundant_role)
212
+ return apply_disable_builtins(config, options.disable_builtins)
213
+
214
+
215
+ def apply_disable_builtins(
216
+ config: ProjectConfig, disable_builtins: tuple[str, ...]
217
+ ) -> ProjectConfig:
218
+ """Stamp ``disable_builtins`` onto every gateway in ``config``.
219
+
220
+ Applied centrally (like :func:`mark_redundant`) so one rule serves every
221
+ profile and the wizard. Uniform across gateways: the demo intent is to drop
222
+ a module everywhere, and a redundant pair must agree on its module set. The
223
+ resolver later copies the list onto any expanded backup node.
224
+ """
225
+ if not disable_builtins:
226
+ return config
227
+ # pydantic does not re-validate on attribute assignment, so validate here -
228
+ # this is the wizard/CLI choke point (the declarative path is checked at
229
+ # construction). Raises ValueError on an unknown slug; the CLI maps that to
230
+ # exit code 2.
231
+ from ignition_stack.catalog.builtins import validate_disable_slugs
232
+
233
+ validate_disable_slugs(list(disable_builtins))
234
+ for gw in config.gateways:
235
+ gw.disable_builtins = list(disable_builtins)
236
+ return config
@@ -111,6 +111,7 @@ def _expand_redundancy(config: ProjectConfig) -> None:
111
111
  memory_mb=master.memory_mb,
112
112
  http_port=next_port,
113
113
  modules=list(master.modules),
114
+ disable_builtins=list(master.disable_builtins),
114
115
  redundancy=RedundancyConfig(
115
116
  mode="backup",
116
117
  peer=master.name,
@@ -25,8 +25,11 @@ Step order:
25
25
  6. **Redundancy** - for profiles with a single workhorse role (standalone
26
26
  gateway, scaleout backend, hub-and-spoke hub), whether to add a backup
27
27
  node and form a master/backup pair. Defaults off.
28
- 7. **Reverse proxy** - existing/install-Traefik/skip.
29
- 8. **Summary + confirm**.
28
+ 7. **Disable built-ins** - opt-in multi-select to turn off built-in IA
29
+ modules (Vision, SFC, ...) stack-wide via GATEWAY_MODULES_ENABLED.
30
+ Gated behind a confirm; defaults to keeping everything.
31
+ 8. **Reverse proxy** - existing/install-Traefik/skip.
32
+ 9. **Summary + confirm**.
30
33
 
31
34
  Per-gateway env-var overrides (``memory_mb`` etc.) are deferred to Phase 7
32
35
  when the lifecycle/reset commands need them; the gateway model already
@@ -117,6 +120,10 @@ class Prompter(Protocol):
117
120
  def integer(self, message: str, default: int, minimum: int = 0) -> int:
118
121
  """Integer prompt; validates ``>= minimum`` and returns the parsed value."""
119
122
 
123
+ def checkbox(self, message: str, choices: Sequence[tuple[str, str]]) -> list[str]:
124
+ """Multi-select prompt. ``choices`` is ``(value, label)`` pairs; returns
125
+ the list of chosen ``value``\\ s (possibly empty - nothing toggled)."""
126
+
120
127
 
121
128
  @dataclass
122
129
  class WizardOutcome:
@@ -162,6 +169,7 @@ def walk(name: str, prompter: Prompter) -> WizardOutcome:
162
169
  edge_role = _ask_edge_role(prompter, profile_slug)
163
170
  network_split = _ask_network_split(prompter, profile_slug)
164
171
  redundant_role = _ask_redundancy(prompter, profile_slug)
172
+ disable_builtins = _ask_disable_builtins(prompter)
165
173
  reverse_proxy = _ask_reverse_proxy(prompter)
166
174
 
167
175
  options = ProfileOptions(
@@ -173,6 +181,7 @@ def walk(name: str, prompter: Prompter) -> WizardOutcome:
173
181
  reverse_proxy=reverse_proxy,
174
182
  database_kind=db_kind,
175
183
  redundant_role=redundant_role,
184
+ disable_builtins=disable_builtins,
176
185
  )
177
186
 
178
187
  # Hub-and-spoke advisory: ask the user inside the wizard rather than
@@ -255,6 +264,28 @@ def _ask_redundancy(prompter: Prompter, profile_slug: str) -> str | None:
255
264
  return role if make else None
256
265
 
257
266
 
267
+ def _ask_disable_builtins(prompter: Prompter) -> tuple[str, ...]:
268
+ """Optionally pick built-in modules to turn off across the stack.
269
+
270
+ Gated behind a confirm so the common path (keep everything) is one
271
+ keystroke and nobody is forced to scroll a 29-item checklist. Saying yes
272
+ opens a multi-select of every built-in (alphabetical by display name);
273
+ whatever is toggled becomes the stack-wide ``disable_builtins``.
274
+ """
275
+ if not prompter.confirm(
276
+ "Disable any built-in gateway modules (e.g. Vision, SFC)?", default=False
277
+ ):
278
+ return ()
279
+ from ignition_stack.catalog.builtins import default_builtin_catalog
280
+
281
+ catalog = default_builtin_catalog()
282
+ choices = [(m.slug, m.name) for m in sorted(catalog.modules, key=lambda m: m.name.lower())]
283
+ chosen = prompter.checkbox(
284
+ "Select modules to DISABLE (space toggles, enter confirms):", choices
285
+ )
286
+ return tuple(chosen)
287
+
288
+
258
289
  def _ask_database(prompter: Prompter) -> str | None:
259
290
  raw = prompter.select("Database?", _DB_CHOICES, default="postgres")
260
291
  return None if raw == "none" else raw
@@ -355,6 +386,8 @@ def _summarize(config: ProjectConfig, profile_slug: str, options: ProfileOptions
355
386
  f"network split: {'on' if config.network_split else 'off'}",
356
387
  "redundancy : "
357
388
  + (f"{options.redundant_role} (master + backup)" if options.redundant_role else "none"),
389
+ "disabled mods: "
390
+ + (", ".join(options.disable_builtins) if options.disable_builtins else "(none - all on)"),
358
391
  "reverse proxy: "
359
392
  + (
360
393
  f"install Traefik at './{config.reverse_proxy.path}'"
@@ -429,3 +462,11 @@ class QuestionaryPrompter:
429
462
 
430
463
  answer = questionary.text(message, default=str(default), validate=_validate).unsafe_ask()
431
464
  return int(answer)
465
+
466
+ def checkbox(self, message: str, choices: Sequence[tuple[str, str]]) -> list[str]:
467
+ import questionary
468
+
469
+ q_choices = [questionary.Choice(title=label, value=value) for value, label in choices]
470
+ answer = questionary.checkbox(message, choices=q_choices).unsafe_ask()
471
+ # Questionary returns None on Ctrl-C and a list otherwise; normalize.
472
+ return [str(a) for a in (answer or [])]
@@ -54,9 +54,10 @@ packages = ["ignition_stack"]
54
54
 
55
55
  [tool.hatch.build.targets.wheel.force-include]
56
56
  "modules.yaml" = "ignition_stack/modules.yaml"
57
+ "builtin_modules.yaml" = "ignition_stack/builtin_modules.yaml"
57
58
 
58
59
  [tool.hatch.build.targets.sdist]
59
- include = ["ignition_stack", "tests", "README.md", "pyproject.toml", "modules.yaml"]
60
+ include = ["ignition_stack", "tests", "README.md", "pyproject.toml", "modules.yaml", "builtin_modules.yaml"]
60
61
 
61
62
  [tool.ruff]
62
63
  line-length = 100