ignition-stack 0.3.0__tar.gz → 0.5.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 (189) hide show
  1. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/.gitignore +3 -0
  2. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/PKG-INFO +1 -1
  3. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/builtin_modules.yaml +38 -0
  4. ignition_stack-0.5.0/ignition_stack/__init__.py +1 -0
  5. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/catalog/builtins.py +46 -10
  6. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/catalog/download.py +4 -14
  7. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/catalog/schema.py +5 -18
  8. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/cli.py +105 -35
  9. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/commands/modules.py +1 -2
  10. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/completion.py +16 -0
  11. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/compose/engine.py +134 -77
  12. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/compose/templates/services/ignition.yaml.j2 +9 -9
  13. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/compose/writer.py +203 -48
  14. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/config/__init__.py +4 -0
  15. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/config/io.py +2 -7
  16. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/config/schema.py +283 -31
  17. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/postsetup/generator.py +117 -26
  18. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/profiles/__init__.py +2 -0
  19. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/profiles/base.py +116 -12
  20. ignition_stack-0.5.0/ignition_stack/profiles/carry.py +319 -0
  21. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/profiles/hub_and_spoke.py +3 -0
  22. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/profiles/scaleout.py +10 -6
  23. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/services/loader.py +1 -3
  24. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/services/manifest.py +82 -2
  25. ignition_stack-0.5.0/ignition_stack/services/resolver.py +361 -0
  26. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/default-namespace/Sparkplug B/config.json +8 -0
  27. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/default-namespace/Sparkplug B/resource.json +17 -0
  28. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/general/config.json +21 -0
  29. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/general/resource.json +16 -0
  30. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/namespace-server-set/Sparkplug B-Default Set/config.json +4 -0
  31. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/namespace-server-set/Sparkplug B-Default Set/resource.json +17 -0
  32. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/server/Chariot SCADA/config.json.j2 +24 -0
  33. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/server/Chariot SCADA/resource.json +19 -0
  34. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/server-set/Default Set/config.json +4 -0
  35. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/server-set/Default Set/resource.json +17 -0
  36. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/general/config.json +4 -0
  37. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/general/resource.json +16 -0
  38. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/history-store/Default In-Memory Store/config.json +17 -0
  39. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/history-store/Default In-Memory Store/resource.json +18 -0
  40. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/server/Chariot SCADA/config.json.j2 +36 -0
  41. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/server/Chariot SCADA/resource.json +19 -0
  42. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/server-set/Default/config.json +5 -0
  43. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/server-set/Default/resource.json +17 -0
  44. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/transmitter/{{gateway}}/config.json.j2 +29 -0
  45. ignition_stack-0.5.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/transmitter/{{gateway}}/resource.json +14 -0
  46. ignition_stack-0.5.0/ignition_stack/templates/post-setup/gateway-network-link.md.j2 +23 -0
  47. ignition_stack-0.5.0/ignition_stack/templates/post-setup/identity-provider.md.j2 +25 -0
  48. ignition_stack-0.5.0/ignition_stack/templates/post-setup/mqtt-engine-connection.md.j2 +106 -0
  49. ignition_stack-0.5.0/ignition_stack/templates/services/chariot/compose.yaml.j2 +39 -0
  50. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/chariot/manifest.yaml +8 -0
  51. ignition_stack-0.5.0/ignition_stack/templates/services/chariot/seed/service/chariot-trial.sh +56 -0
  52. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/emqx/manifest.yaml +9 -0
  53. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/hivemq/manifest.yaml +8 -0
  54. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/keycloak/compose.yaml.j2 +2 -0
  55. ignition_stack-0.5.0/ignition_stack/templates/services/keycloak/manifest.yaml +32 -0
  56. ignition_stack-0.5.0/ignition_stack/templates/services/keycloak/seed/gateway-resources/config/resources/core/ignition/identity-provider/keycloak/config.json +57 -0
  57. ignition_stack-0.5.0/ignition_stack/templates/services/keycloak/seed/gateway-resources/config/resources/core/ignition/identity-provider/keycloak/resource.json +18 -0
  58. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/keycloak/seed/service/import/ignition-realm.json +23 -2
  59. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/mariadb/manifest.yaml +4 -0
  60. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/mongo/manifest.yaml +4 -0
  61. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/mysql/manifest.yaml +4 -0
  62. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/postgres/manifest.yaml +7 -0
  63. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/rabbitmq/manifest.yaml +8 -0
  64. ignition_stack-0.5.0/ignition_stack/update_check.py +129 -0
  65. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/wizard.py +220 -58
  66. ignition_stack-0.5.0/ignition_stack/wizard_composer.py +541 -0
  67. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/modules.yaml +7 -7
  68. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/pyproject.toml +1 -1
  69. ignition_stack-0.5.0/tests/golden/combos/hub-and-spoke-iiot-chariot/docker-compose.yaml +184 -0
  70. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/combos/network-split/docker-compose.yaml +2 -0
  71. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/profiles/hub-and-spoke/docker-compose.yaml +29 -6
  72. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/profiles/scaleout/docker-compose.yaml +13 -0
  73. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/profiles/scaleout-redundant/docker-compose.yaml +8 -0
  74. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/scaleout-skeleton/docker-compose.yaml +0 -2
  75. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/chariot/docker-compose.yaml +16 -0
  76. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/keycloak/docker-compose.yaml +2 -2
  77. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_compose_engine.py +25 -23
  78. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_declarative_io.py +147 -6
  79. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_disable_builtins.py +65 -13
  80. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_docs_cli_reference.py +1 -3
  81. ignition_stack-0.5.0/tests/test_iiot_overlay.py +384 -0
  82. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_init_standalone.py +4 -32
  83. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_lifecycle.py +1 -3
  84. ignition_stack-0.5.0/tests/test_manifest_invariants.py +304 -0
  85. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_modules_catalog.py +1 -3
  86. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_modules_cli.py +1 -3
  87. ignition_stack-0.5.0/tests/test_postsetup.py +156 -0
  88. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_profiles.py +292 -66
  89. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_redundancy.py +5 -16
  90. ignition_stack-0.5.0/tests/test_registry.py +289 -0
  91. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_service_catalog.py +117 -35
  92. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_service_catalog_smoke.py +1 -3
  93. ignition_stack-0.5.0/tests/test_switch_profile_registry.py +320 -0
  94. ignition_stack-0.5.0/tests/test_update_check.py +108 -0
  95. ignition_stack-0.5.0/tests/test_wizard_composer.py +448 -0
  96. ignition_stack-0.5.0/verification/iiot-spike/README.md +142 -0
  97. ignition_stack-0.3.0/ignition_stack/__init__.py +0 -1
  98. ignition_stack-0.3.0/ignition_stack/services/resolver.py +0 -186
  99. ignition_stack-0.3.0/ignition_stack/templates/post-setup/gateway-network-link.md.j2 +0 -18
  100. ignition_stack-0.3.0/ignition_stack/templates/post-setup/identity-provider.md.j2 +0 -13
  101. ignition_stack-0.3.0/ignition_stack/templates/post-setup/mqtt-engine-connection.md.j2 +0 -11
  102. ignition_stack-0.3.0/ignition_stack/templates/services/chariot/compose.yaml.j2 +0 -17
  103. ignition_stack-0.3.0/ignition_stack/templates/services/keycloak/manifest.yaml +0 -25
  104. ignition_stack-0.3.0/tests/test_postsetup.py +0 -84
  105. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/LICENSE +0 -0
  106. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/README.md +0 -0
  107. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/catalog/__init__.py +0 -0
  108. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/catalog/loader.py +0 -0
  109. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/catalog/verify.py +0 -0
  110. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/commands/__init__.py +0 -0
  111. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/compose/__init__.py +0 -0
  112. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/compose/templates/footer.yaml.j2 +0 -0
  113. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/compose/templates/header.yaml.j2 +0 -0
  114. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/compose/templates/services/bootstrap.yaml.j2 +0 -0
  115. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/lifecycle/__init__.py +0 -0
  116. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/lifecycle/cleanup.py +0 -0
  117. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/lifecycle/record.py +0 -0
  118. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/lifecycle/regenerate.py +0 -0
  119. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/postsetup/__init__.py +0 -0
  120. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/profiles/advisory.py +0 -0
  121. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/profiles/mcp_n8n.py +0 -0
  122. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/profiles/standalone.py +0 -0
  123. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/services/__init__.py +0 -0
  124. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/__init__.py +0 -0
  125. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/post-setup/_default.md.j2 +0 -0
  126. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/post-setup/device-connection.md.j2 +0 -0
  127. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/post-setup/kafka-connector.md.j2 +0 -0
  128. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/post-setup/mcp-module.md.j2 +0 -0
  129. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/post-setup/opc-ua-connection.md.j2 +0 -0
  130. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/post-setup/redundancy-pairing.md.j2 +0 -0
  131. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/post-setup/reverse-proxy.md.j2 +0 -0
  132. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/redundancy/redundancy.xml.j2 +0 -0
  133. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/chariot/seed/service/USAGE.md +0 -0
  134. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/emqx/compose.yaml.j2 +0 -0
  135. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/emqx/seed/service/USAGE.md +0 -0
  136. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/hivemq/compose.yaml.j2 +0 -0
  137. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/hivemq/seed/service/USAGE.md +0 -0
  138. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/kafka/compose.yaml.j2 +0 -0
  139. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/kafka/manifest.yaml +0 -0
  140. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/kafka/seed/service/USAGE.md +0 -0
  141. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/mariadb/compose.yaml.j2 +0 -0
  142. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/mariadb/seed/service/initdb/00-create-extra-databases.sh +0 -0
  143. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/modbus-sim/compose.yaml.j2 +0 -0
  144. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/modbus-sim/manifest.yaml +0 -0
  145. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/modbus-sim/seed/service/USAGE.md +0 -0
  146. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/mongo/compose.yaml.j2 +0 -0
  147. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/mongo/seed/service/initdb/01-demo-collection.js +0 -0
  148. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/mysql/compose.yaml.j2 +0 -0
  149. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/mysql/seed/service/initdb/00-create-extra-databases.sh +0 -0
  150. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/n8n/compose.yaml.j2 +0 -0
  151. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/n8n/manifest.yaml +0 -0
  152. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/n8n/seed/service/USAGE.md +0 -0
  153. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/opcua-sim/compose.yaml.j2 +0 -0
  154. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/opcua-sim/manifest.yaml +0 -0
  155. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/opcua-sim/seed/service/USAGE.md +0 -0
  156. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/postgres/compose.yaml.j2 +0 -0
  157. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/config.json +0 -0
  158. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/resource.json +0 -0
  159. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/secret-provider/internal-secret-provider/config.json +0 -0
  160. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/secret-provider/internal-secret-provider/resource.json +0 -0
  161. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/postgres/seed/service/initdb/00-create-extra-databases.sh +0 -0
  162. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/rabbitmq/compose.yaml.j2 +0 -0
  163. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/services/rabbitmq/seed/service/enabled_plugins +0 -0
  164. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/standalone-postgres/docker-compose.yaml +0 -0
  165. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/standalone-postgres/scripts/docker-bootstrap.sh +0 -0
  166. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/standalone-postgres/services/ignition/config/resources/core/config-mode.json +0 -0
  167. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/standalone-postgres/services/ignition/config/resources/dev/config-mode.json +0 -0
  168. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/ignition_stack/templates/standalone-postgres/services/ignition/projects/.gitkeep +0 -0
  169. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/__init__.py +0 -0
  170. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/conftest.py +0 -0
  171. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/combos/smoke-stack/docker-compose.yaml +0 -0
  172. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/profiles/mcp-n8n/docker-compose.yaml +0 -0
  173. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/profiles/standalone/docker-compose.yaml +0 -0
  174. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/db-mariadb/docker-compose.yaml +0 -0
  175. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/db-mongo/docker-compose.yaml +0 -0
  176. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/db-mysql/docker-compose.yaml +0 -0
  177. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/db-postgres/docker-compose.yaml +0 -0
  178. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/emqx/docker-compose.yaml +0 -0
  179. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/hivemq/docker-compose.yaml +0 -0
  180. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/kafka/docker-compose.yaml +0 -0
  181. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/modbus-sim/docker-compose.yaml +0 -0
  182. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/n8n/docker-compose.yaml +0 -0
  183. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/opcua-sim/docker-compose.yaml +0 -0
  184. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/services/rabbitmq/docker-compose.yaml +0 -0
  185. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/golden/standalone-postgres/docker-compose.yaml +0 -0
  186. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_builtin_catalog_smoke.py +0 -0
  187. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/tests/test_completion.py +0 -0
  188. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/verification/redundancy-spike/README.md +0 -0
  189. {ignition_stack-0.3.0 → ignition_stack-0.5.0}/verification/smoke/README.md +0 -0
@@ -30,3 +30,6 @@ Thumbs.db
30
30
  .env
31
31
  .env.local
32
32
  *.env
33
+
34
+ # Tool artifacts
35
+ .playwright-mcp/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ignition-stack
3
- Version: 0.3.0
3
+ Version: 0.5.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
@@ -25,6 +25,15 @@
25
25
  # `identifier` is the fully-qualified module id used verbatim in
26
26
  # GATEWAY_MODULES_ENABLED. `slug` is the friendly kebab name a user puts in
27
27
  # `disable_builtins`. `name` is the gateway's display name (for wizard labels).
28
+ #
29
+ # `default_enabled` marks the curated "typical demo" set the interactive wizard
30
+ # pre-checks (opt-in selection). It does NOT change the engine math or the
31
+ # non-interactive profile path - profiles still enable every built-in. It only
32
+ # seeds the wizard's checkbox so the common path is a lean gateway. The JDBC
33
+ # drivers are all `default_enabled: false` on purpose: the wizard enables the
34
+ # one matching the chosen database (see `jdbc_driver_for`), not a static one.
35
+ # The smoke guard checks the module *set*, not this curation flag, so the
36
+ # default-set invariants live in tests/test_disable_builtins.py instead.
28
37
 
29
38
  version: 1
30
39
 
@@ -36,87 +45,116 @@ modules:
36
45
  - slug: alarm-notification
37
46
  identifier: com.inductiveautomation.alarm-notification
38
47
  name: Alarm Notification
48
+ default_enabled: true
39
49
  - slug: allen-bradley-driver
40
50
  identifier: com.inductiveautomation.opcua.drivers.ablegacy
41
51
  name: Allen-Bradley Driver
52
+ default_enabled: false
42
53
  - slug: bacnet-driver
43
54
  identifier: com.inductiveautomation.opcua.drivers.bacnet
44
55
  name: BACnet Driver
56
+ default_enabled: false
45
57
  - slug: enterprise-administration
46
58
  identifier: com.inductiveautomation.eam
47
59
  name: Enterprise Administration
60
+ default_enabled: false
48
61
  - slug: event-streams
49
62
  identifier: com.inductiveautomation.eventstream
50
63
  name: Event Streams
64
+ default_enabled: false
51
65
  - slug: historian-core
52
66
  identifier: com.inductiveautomation.historian
53
67
  name: Historian Core
68
+ default_enabled: true
54
69
  - slug: kafka-connector
55
70
  identifier: com.inductiveautomation.connectors.kafka
56
71
  name: Kafka Connector
72
+ default_enabled: false
57
73
  - slug: legacy-dnp3-driver
58
74
  identifier: com.inductiveautomation.opcua.drivers.dnp3
59
75
  name: Legacy DNP3 Driver
76
+ default_enabled: false
60
77
  - slug: logix-driver
61
78
  identifier: com.inductiveautomation.opcua.drivers.logix
62
79
  name: Logix Driver
80
+ default_enabled: false
63
81
  - slug: mariadb-jdbc-driver
64
82
  identifier: com.inductiveautomation.jdbc.mariadb
65
83
  name: MariaDB JDBC Driver
84
+ default_enabled: false
66
85
  - slug: micro800-driver
67
86
  identifier: com.inductiveautomation.opcua.drivers.micro800
68
87
  name: Micro800 Driver
88
+ default_enabled: false
69
89
  - slug: mitsubishi-driver
70
90
  identifier: com.inductiveautomation.opcua.drivers.mitsubishi
71
91
  name: Mitsubishi Driver
92
+ default_enabled: false
72
93
  - slug: modbus-driver
73
94
  identifier: com.inductiveautomation.opcua.drivers.modbus
74
95
  name: Modbus Driver
96
+ default_enabled: false
75
97
  - slug: mssql-jdbc-driver
76
98
  identifier: com.inductiveautomation.jdbc.mssql
77
99
  name: MSSQL JDBC Driver
100
+ default_enabled: false
78
101
  - slug: omron-driver
79
102
  identifier: com.inductiveautomation.opcua.drivers.omron
80
103
  name: Omron Driver
104
+ default_enabled: false
81
105
  - slug: opc-ua
82
106
  identifier: com.inductiveautomation.opcua
83
107
  name: OPC-UA
108
+ default_enabled: true
84
109
  - slug: perspective
85
110
  identifier: com.inductiveautomation.perspective
86
111
  name: Perspective
112
+ default_enabled: true
87
113
  - slug: postgresql-jdbc-driver
88
114
  identifier: com.inductiveautomation.jdbc.postgresql
89
115
  name: PostgreSQL JDBC Driver
116
+ default_enabled: false
90
117
  - slug: reporting
91
118
  identifier: com.inductiveautomation.reporting
92
119
  name: Reporting
120
+ default_enabled: true
93
121
  - slug: sfc
94
122
  identifier: com.inductiveautomation.sfc
95
123
  name: SFC
124
+ default_enabled: false
96
125
  - slug: siemens-drivers
97
126
  identifier: com.inductiveautomation.opcua.drivers.siemens
98
127
  name: Siemens Drivers
128
+ default_enabled: false
99
129
  - slug: siemens-enhanced-driver
100
130
  identifier: com.inductiveautomation.opcua.drivers.siemens-symbolic
101
131
  name: Siemens Enhanced Driver
132
+ default_enabled: false
102
133
  - slug: sms-notification
103
134
  identifier: com.inductiveautomation.sms-notification
104
135
  name: SMS Notification
136
+ default_enabled: false
105
137
  - slug: sql-bridge
106
138
  identifier: com.inductiveautomation.sqlbridge
107
139
  name: SQL Bridge
140
+ default_enabled: true
108
141
  - slug: sql-historian
109
142
  identifier: com.inductiveautomation.historian.sql
110
143
  name: SQL Historian
144
+ default_enabled: true
111
145
  - slug: symbol-factory
112
146
  identifier: com.inductiveautomation.symbol-factory
113
147
  name: Symbol Factory
148
+ default_enabled: false
114
149
  - slug: udp-and-tcp-drivers
115
150
  identifier: com.inductiveautomation.opcua.drivers.tcpudp
116
151
  name: UDP and TCP Drivers
152
+ default_enabled: false
117
153
  - slug: vision
118
154
  identifier: com.inductiveautomation.vision
119
155
  name: Vision
156
+ default_enabled: false
120
157
  - slug: webdev
121
158
  identifier: com.inductiveautomation.webdev
122
159
  name: WebDev
160
+ default_enabled: false
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
@@ -52,6 +52,15 @@ class BuiltinModule(BaseModel):
52
52
  ),
53
53
  ]
54
54
  name: Annotated[str, Field(min_length=1, description="Gateway display name (wizard label).")]
55
+ default_enabled: Annotated[
56
+ bool,
57
+ Field(
58
+ default=False,
59
+ description=(
60
+ "Whether the wizard pre-checks this module in its opt-in selection. Curation only - it does not change the engine math or the non-interactive profile path."
61
+ ),
62
+ ),
63
+ ]
55
64
 
56
65
 
57
66
  class BuiltinCatalog(BaseModel):
@@ -80,6 +89,17 @@ class BuiltinCatalog(BaseModel):
80
89
  """Every known built-in slug."""
81
90
  return {m.slug for m in self.modules}
82
91
 
92
+ @property
93
+ def default_enabled_slugs(self) -> set[str]:
94
+ """Slugs the wizard pre-checks in its opt-in module selection.
95
+
96
+ Curation only: this seeds the wizard checkbox and never feeds the
97
+ engine math or the non-interactive profile path. The matching JDBC
98
+ driver is added on top by :func:`jdbc_driver_for`, so JDBC drivers are
99
+ deliberately absent here (they are database-driven, not statically on).
100
+ """
101
+ return {m.slug for m in self.modules if m.default_enabled}
102
+
83
103
  def identifiers_excluding(self, disabled_slugs: list[str]) -> list[str]:
84
104
  """FQ identifiers of every built-in whose slug is not in ``disabled_slugs``.
85
105
 
@@ -109,9 +129,7 @@ def load_builtin_catalog(path: Path | None = None) -> BuiltinCatalog:
109
129
  try:
110
130
  return BuiltinCatalog.model_validate(raw)
111
131
  except ValidationError as exc:
112
- raise BuiltinCatalogLoadError(
113
- f"{DEFAULT_BUILTIN_CATALOG_NAME} failed schema validation:\n{exc}"
114
- ) from exc
132
+ raise BuiltinCatalogLoadError(f"{DEFAULT_BUILTIN_CATALOG_NAME} failed schema validation:\n{exc}") from exc
115
133
 
116
134
 
117
135
  @lru_cache(maxsize=1)
@@ -130,6 +148,29 @@ def builtin_slugs() -> frozenset[str]:
130
148
  return frozenset(default_builtin_catalog().slugs)
131
149
 
132
150
 
151
+ # Which built-in JDBC driver the wizard enables for each database kind. The
152
+ # catalog ships no MySQL-specific driver - Ignition connects to MySQL with the
153
+ # wire-compatible MariaDB driver, so both map to it. Mongo (not a JDBC store)
154
+ # and an absent database have no entry and resolve to None.
155
+ _JDBC_DRIVER_FOR_DB: dict[str, str] = {
156
+ "postgres": "postgresql-jdbc-driver",
157
+ "mariadb": "mariadb-jdbc-driver",
158
+ "mysql": "mariadb-jdbc-driver",
159
+ }
160
+
161
+
162
+ def jdbc_driver_for(db_kind: str | None) -> str | None:
163
+ """The built-in JDBC driver slug the wizard enables for ``db_kind``.
164
+
165
+ Data-driven so the wizard stays declarative: postgres/mariadb map to their
166
+ own driver, mysql reuses the MariaDB driver, and mongo / no-database get
167
+ none. Returns ``None`` when no driver applies.
168
+ """
169
+ if db_kind is None:
170
+ return None
171
+ return _JDBC_DRIVER_FOR_DB.get(db_kind)
172
+
173
+
133
174
  def validate_disable_slugs(slugs: list[str]) -> None:
134
175
  """Raise ``ValueError`` if any slug is not a known built-in.
135
176
 
@@ -141,10 +182,7 @@ def validate_disable_slugs(slugs: list[str]) -> None:
141
182
  known = builtin_slugs()
142
183
  unknown = [s for s in slugs if s not in known]
143
184
  if unknown:
144
- raise ValueError(
145
- f"unknown built-in module slug(s): {', '.join(unknown)}. "
146
- f"Valid slugs are: {', '.join(sorted(known))}"
147
- )
185
+ raise ValueError(f"unknown built-in module slug(s): {', '.join(unknown)}. Valid slugs are: {', '.join(sorted(known))}")
148
186
 
149
187
 
150
188
  def _read_yaml_text(path: Path | None) -> str:
@@ -165,7 +203,5 @@ def _read_yaml_text(path: Path | None) -> str:
165
203
  repo_root = Path(__file__).resolve().parents[2]
166
204
  dev_path = repo_root / DEFAULT_BUILTIN_CATALOG_NAME
167
205
  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
- )
206
+ raise BuiltinCatalogLoadError(f"{DEFAULT_BUILTIN_CATALOG_NAME} not found in package data or at {dev_path}.")
171
207
  return dev_path.read_text(encoding="utf-8")
@@ -64,11 +64,7 @@ def download_entry(
64
64
  if entry.requires_manual_download:
65
65
  return _handle_manual(entry, target)
66
66
 
67
- if (
68
- target.exists()
69
- and entry.sha256 != SHA256_UNPINNED
70
- and sha256_of_file(target) == entry.sha256
71
- ):
67
+ if target.exists() and entry.sha256 != SHA256_UNPINNED and sha256_of_file(target) == entry.sha256:
72
68
  return DownloadResult(
73
69
  entry.name,
74
70
  DownloadOutcome.SKIPPED_CACHED,
@@ -78,8 +74,7 @@ def download_entry(
78
74
 
79
75
  if offline:
80
76
  raise DownloadError(
81
- f"{entry.name}: --offline set but artifact not in cache "
82
- f"({target}). Pre-populate the cache or drop --offline.",
77
+ f"{entry.name}: --offline set but artifact not in cache " f"({target}). Pre-populate the cache or drop --offline.",
83
78
  )
84
79
 
85
80
  if entry.download_url is None:
@@ -94,8 +89,7 @@ def download_entry(
94
89
  if actual != entry.sha256:
95
90
  target.unlink(missing_ok=True)
96
91
  raise DownloadError(
97
- f"{entry.name}: sha256 mismatch after download "
98
- f"(expected {entry.sha256}, got {actual}). Cached file removed.",
92
+ f"{entry.name}: sha256 mismatch after download " f"(expected {entry.sha256}, got {actual}). Cached file removed.",
99
93
  )
100
94
 
101
95
  return DownloadResult(
@@ -121,11 +115,7 @@ def _handle_manual(entry: CatalogEntry, target: Path) -> DownloadResult:
121
115
  entry.name,
122
116
  DownloadOutcome.SKIPPED_MANUAL,
123
117
  None,
124
- (
125
- f"WARN: {entry.name} local_source_path missing ({source}). "
126
- "Skipping: requires manual download. "
127
- "See POST-SETUP.md for instructions."
128
- ),
118
+ (f"WARN: {entry.name} local_source_path missing ({source}). " "Skipping: requires manual download. " "See POST-SETUP.md for instructions."),
129
119
  )
130
120
 
131
121
  shutil.copy2(source, target)
@@ -65,35 +65,24 @@ class _EntryBase(BaseModel):
65
65
  str,
66
66
  Field(
67
67
  pattern=rf"^([0-9a-f]{{64}}|{SHA256_UNPINNED})$",
68
- description=(
69
- f"Lowercase hex sha256 of the artifact, or '{SHA256_UNPINNED}' "
70
- "while a maintainer is mid-bump (rejected by `modules validate`)."
71
- ),
68
+ description=(f"Lowercase hex sha256 of the artifact, or '{SHA256_UNPINNED}' " "while a maintainer is mid-bump (rejected by `modules validate`)."),
72
69
  ),
73
70
  ]
74
71
  install_path: Annotated[
75
72
  str,
76
73
  Field(
77
74
  min_length=1,
78
- description=(
79
- "Fully-qualified in-container destination path. The compose "
80
- "layer mounts/copies the cached artifact here."
81
- ),
75
+ description=("Fully-qualified in-container destination path. The compose " "layer mounts/copies the cached artifact here."),
82
76
  ),
83
77
  ]
84
78
  requires_license_env: str | None = Field(
85
79
  default=None,
86
- description=(
87
- "Name of an env var the user must set with their license key. "
88
- "None for community-usable modules and unlicensed drivers."
89
- ),
80
+ description=("Name of an env var the user must set with their license key. " "None for community-usable modules and unlicensed drivers."),
90
81
  )
91
82
  requires_manual_download: bool = Field(
92
83
  default=False,
93
84
  description=(
94
- "True when the artifact has no public URL (e.g. EA-gated). "
95
- "`modules download` skips these unless local_source_path is set "
96
- "and points at an existing file."
85
+ "True when the artifact has no public URL (e.g. EA-gated). " "`modules download` skips these unless local_source_path is set " "and points at an existing file."
97
86
  ),
98
87
  )
99
88
  local_source_path: str | None = Field(
@@ -121,9 +110,7 @@ class ModuleEntry(_EntryBase):
121
110
  min_length=1,
122
111
  pattern=r"^[a-z0-9.]+$",
123
112
  description=(
124
- "Fully-qualified module identifier (e.g. "
125
- "'com.cirruslink.mqtt.engine.gateway'). Used verbatim in "
126
- "ACCEPT_MODULE_LICENSES and ACCEPT_MODULE_CERTS. NOT a path."
113
+ "Fully-qualified module identifier (e.g. " "'com.cirruslink.mqtt.engine.gateway'). Used verbatim in " "ACCEPT_MODULE_LICENSES and ACCEPT_MODULE_CERTS. NOT a path."
127
114
  ),
128
115
  ),
129
116
  ]
@@ -24,6 +24,7 @@ from ignition_stack.commands.modules import modules_app
24
24
  from ignition_stack.completion import (
25
25
  complete_disable_builtin,
26
26
  complete_edge_role,
27
+ complete_iiot_broker,
27
28
  complete_output_format,
28
29
  complete_profile,
29
30
  complete_redundant_role,
@@ -55,7 +56,19 @@ from ignition_stack.profiles import (
55
56
  get_profile,
56
57
  list_profiles,
57
58
  )
59
+ from ignition_stack.profiles.carry import (
60
+ carry_registry,
61
+ database_carried_by_kind,
62
+ detect_iiot_broker,
63
+ is_default_representable,
64
+ )
65
+ from ignition_stack.services.loader import load_all_services
58
66
  from ignition_stack.services.resolver import resolve
67
+ from ignition_stack.update_check import (
68
+ check_for_update,
69
+ detect_upgrade_command,
70
+ should_notify,
71
+ )
59
72
  from ignition_stack.wizard import run_wizard
60
73
 
61
74
  app = typer.Typer(
@@ -89,6 +102,26 @@ def _root(
89
102
  if ctx.invoked_subcommand is None:
90
103
  console.print(ctx.get_help())
91
104
  raise typer.Exit()
105
+ _notify_update_available()
106
+
107
+
108
+ def _notify_update_available() -> None:
109
+ """Print a one-line advisory when a newer release is on PyPI.
110
+
111
+ Runs only for real subcommands (not --version or bare help) and only on an
112
+ interactive terminal. Best-effort: any failure inside the check is swallowed
113
+ rather than allowed to disrupt the command the user actually ran.
114
+ """
115
+ if not should_notify():
116
+ return
117
+ result = check_for_update()
118
+ if result is None:
119
+ return
120
+ current, latest = result
121
+ console.print(
122
+ f"[dim]update available[/dim] [yellow]{current}[/yellow] -> " f"[green]{latest}[/green] · run: [cyan]{detect_upgrade_command()}[/cyan]",
123
+ highlight=False,
124
+ )
92
125
 
93
126
 
94
127
  def _profile_help() -> str:
@@ -127,18 +160,12 @@ def init(
127
160
  network_split: bool | None = typer.Option(
128
161
  None,
129
162
  "--network-split/--no-network-split",
130
- help=(
131
- "Force the frontend/backend network split on or off. Default follows "
132
- "the profile (scaleout splits, hub-and-spoke does not)."
133
- ),
163
+ help=("Force the frontend/backend network split on or off. Default follows " "the profile (scaleout splits, hub-and-spoke does not)."),
134
164
  ),
135
165
  reverse_proxy: str | None = typer.Option(
136
166
  None,
137
167
  "--reverse-proxy",
138
- help=(
139
- "Scaffold a reverse proxy of the given kind ('traefik'). Lays down a "
140
- "README + POST-SETUP entry at --proxy-path. Omit for plain host-port mapping."
141
- ),
168
+ help=("Scaffold a reverse proxy of the given kind ('traefik'). Lays down a " "README + POST-SETUP entry at --proxy-path. Omit for plain host-port mapping."),
142
169
  autocompletion=complete_reverse_proxy,
143
170
  ),
144
171
  proxy_path: str = typer.Option(
@@ -184,6 +211,26 @@ def init(
184
211
  ),
185
212
  autocompletion=complete_disable_builtin,
186
213
  ),
214
+ iiot: bool = typer.Option(
215
+ False,
216
+ "--iiot/--no-iiot",
217
+ help=(
218
+ "Overlay an MQTT/Sparkplug IIoT pipeline: add a broker and wire the "
219
+ "Cirrus Link Transmission/Engine modules across the gateways by role "
220
+ "(spokes/frontends transmit, hub/backend run Engine; a single gateway "
221
+ "runs both). Defaults the broker to 'chariot'."
222
+ ),
223
+ ),
224
+ iiot_broker: str | None = typer.Option(
225
+ None,
226
+ "--iiot-broker",
227
+ help=(
228
+ "MQTT broker slug the IIoT overlay wires to (implies --iiot). Must be "
229
+ "a catalog 'mqtt-broker' kind (e.g. 'chariot', 'emqx', 'hivemq'). "
230
+ "Defaults to 'chariot' when --iiot is given without this flag."
231
+ ),
232
+ autocompletion=complete_iiot_broker,
233
+ ),
187
234
  from_file: Path | None = typer.Option( # noqa: B008 - Typer pattern
188
235
  None,
189
236
  "--from-file",
@@ -253,6 +300,8 @@ def init(
253
300
  proxy_path=proxy_path,
254
301
  redundant=redundant,
255
302
  disable_builtin=disable_builtin,
303
+ iiot=iiot,
304
+ iiot_broker=iiot_broker,
256
305
  )
257
306
 
258
307
  if dry_run:
@@ -274,19 +323,12 @@ def init(
274
323
  console.print("Next steps:")
275
324
  console.print(f" cd {target}")
276
325
  console.print(" docker compose up -d")
277
- console.print(
278
- f" open http://localhost:{config.gateways[0].http_port} (admin / {config.admin_password})"
279
- )
326
+ console.print(f" open http://localhost:{config.gateways[0].http_port} (admin / {config.admin_password})")
280
327
  console.print()
281
- console.print(
282
- f" config recorded in {LIFECYCLE_DIR}/ - run `ignition-stack reset` to "
283
- "regenerate or `switch-profile <name>` to reshape this stack."
284
- )
328
+ console.print(f" config recorded in {LIFECYCLE_DIR}/ - run `ignition-stack reset` to " "regenerate or `switch-profile <name>` to reshape this stack.")
285
329
 
286
330
 
287
- def _validate_init_flags(
288
- *, profile: str | None, from_file: Path | None, dry_run: bool, fmt: str | None
289
- ) -> None:
331
+ def _validate_init_flags(*, profile: str | None, from_file: Path | None, dry_run: bool, fmt: str | None) -> None:
290
332
  """Enforce the mutual-exclusion + flag-applicability rules, or exit code 2.
291
333
 
292
334
  ``--from-file`` already fully specifies the topology, so combining it with
@@ -296,18 +338,13 @@ def _validate_init_flags(
296
338
  supported formats here so a bad ``--output-format`` fails before any build.
297
339
  """
298
340
  if from_file is not None and profile is not None:
299
- console.print(
300
- "[red]error[/red]: --from-file cannot be combined with --profile; a "
301
- "config file already specifies the full topology."
302
- )
341
+ console.print("[red]error[/red]: --from-file cannot be combined with --profile; a " "config file already specifies the full topology.")
303
342
  raise typer.Exit(code=2)
304
343
  if fmt is not None and not dry_run:
305
344
  console.print("[red]error[/red]: --output-format only applies with --dry-run.")
306
345
  raise typer.Exit(code=2)
307
346
  if fmt is not None and fmt not in {"yaml", "json"}:
308
- console.print(
309
- f"[red]error[/red]: unsupported --output-format '{fmt}'; use 'yaml' or 'json'."
310
- )
347
+ console.print(f"[red]error[/red]: unsupported --output-format '{fmt}'; use 'yaml' or 'json'.")
311
348
  raise typer.Exit(code=2)
312
349
 
313
350
 
@@ -342,6 +379,8 @@ def _build_from_profile(
342
379
  proxy_path: str,
343
380
  redundant: str | None,
344
381
  disable_builtin: list[str],
382
+ iiot: bool,
383
+ iiot_broker: str | None,
345
384
  ) -> ProjectConfig:
346
385
  """Materialize a config from the named profile + CLI flags, or exit cleanly."""
347
386
  try:
@@ -351,6 +390,9 @@ def _build_from_profile(
351
390
  raise typer.Exit(code=2) from exc
352
391
 
353
392
  proxy = ReverseProxyConfig(kind=reverse_proxy, path=proxy_path) if reverse_proxy else None
393
+ # --iiot-broker implies --iiot, so naming a broker is enough to turn the
394
+ # overlay on; build_profile defaults the slug to 'chariot' when iiot is on
395
+ # without an explicit broker.
354
396
  options = ProfileOptions(
355
397
  spokes=spokes,
356
398
  frontends=frontends,
@@ -360,6 +402,8 @@ def _build_from_profile(
360
402
  reverse_proxy=proxy,
361
403
  redundant_role=redundant,
362
404
  disable_builtins=tuple(disable_builtin),
405
+ iiot=iiot or iiot_broker is not None,
406
+ iiot_broker=iiot_broker,
363
407
  )
364
408
  try:
365
409
  config = build_profile(profile, name, options)
@@ -448,9 +492,7 @@ def switch_profile(
448
492
  # 'gateway'), which the target profile may not have. Building its base
449
493
  # topology lets us check before build_profile's mark_redundant would reject
450
494
  # it - drop the intent with an advisory rather than failing the reshape.
451
- if options.redundant_role is not None and not can_host_redundant_role(
452
- get_profile(profile).build(current.name, options), options.redundant_role
453
- ):
495
+ if options.redundant_role is not None and not can_host_redundant_role(get_profile(profile).build(current.name, options), options.redundant_role):
454
496
  console.print(
455
497
  f"[yellow]note[/yellow]: redundancy on '{options.redundant_role}' was not "
456
498
  f"carried to {profile} (no matching gateway); re-apply with --redundant "
@@ -466,6 +508,15 @@ def switch_profile(
466
508
  console.print(f"[red]error[/red]: {exc}")
467
509
  raise typer.Exit(code=2) from exc
468
510
 
511
+ # Re-graft the richer registry shapes ProfileOptions can't express (custom
512
+ # ids, per-instance overrides, partial attachment sets, a second database),
513
+ # re-mapping their attachments by role and dropping any that the new topology
514
+ # can't host - each with a printed advisory. Resolve first so the profile's
515
+ # legacy database is already lowered into per-gateway attachments; the carry's
516
+ # one-database-per-gateway guard then sees them and won't over-attach. The
517
+ # carry's output is resolved again by regenerate (resolve is idempotent).
518
+ new_config = carry_registry(resolve(new_config), current, console)
519
+
469
520
  files = regenerate(project_dir, new_config)
470
521
  console.print(f"[green]switched[/green] {current.profile or 'custom'} -> {profile}")
471
522
  console.print(f" {len(files)} file(s) regenerated")
@@ -489,11 +540,7 @@ def _options_from_config(config: ProjectConfig) -> ProfileOptions:
489
540
  # by the resolver), so recover the role/name of whichever gateway is the
490
541
  # master and let the new profile re-expand the pair.
491
542
  redundant_role = next(
492
- (
493
- gw.role or gw.name
494
- for gw in config.gateways
495
- if gw.redundancy is not None and gw.redundancy.mode == "master"
496
- ),
543
+ (gw.role or gw.name for gw in config.gateways if gw.redundancy is not None and gw.redundancy.mode == "master"),
497
544
  None,
498
545
  )
499
546
  # Disabled built-ins are applied stack-wide, so carry over the slugs disabled
@@ -502,16 +549,39 @@ def _options_from_config(config: ProjectConfig) -> ProfileOptions:
502
549
  # one node. The target profile re-applies it uniformly.
503
550
  disabled_sets = [set(gw.disable_builtins) for gw in config.gateways]
504
551
  disable_builtins = tuple(sorted(set.intersection(*disabled_sets))) if disabled_sets else ()
552
+ # A recorded config is resolved: its database + services live in the
553
+ # registry, not the legacy fields. Recover the profile inputs from the
554
+ # registry. The primary database rides database_kind (see below); the
555
+ # non-database instances that are default-representable ride `services`.
556
+ # IIoT intent is recovered from the attachment roles: a stack with any
557
+ # mqtt-transmission/mqtt-engine attachment was built with apply_iiot, so set
558
+ # iiot=True and the broker slug. build_profile re-runs the overlay in the new
559
+ # topology, re-mapping Transmission/Engine onto the new roles naturally; the
560
+ # broker instance is therefore excluded from `services` below to avoid a
561
+ # double-add. Anything richer than `services` can express (custom ids,
562
+ # per-instance overrides, partial/role-specific attachments, a second
563
+ # database) is carried after build_profile by carry_registry.
564
+ iiot_broker = detect_iiot_broker(config)
565
+ catalog = load_all_services()
566
+ representable = tuple(inst.service for inst in config.non_database_instances() if inst.service != iiot_broker and is_default_representable(inst, config, catalog))
567
+ # The primary database rides database_kind only when it has the clean
568
+ # canonical shape (id "db", default image/credentials, consumer on every
569
+ # non-Edge gateway). A custom primary database - or any second database - is
570
+ # left for carry_registry to re-graft, so database_kind stays None and the
571
+ # profile does not also lay down a colliding default DB.
572
+ carried_db = database_carried_by_kind(config, catalog)
505
573
  return ProfileOptions(
506
574
  spokes=spoke_count or 3,
507
575
  frontends=frontend_count or 1,
508
576
  edge_role=edge_roles[0] if edge_roles else "none",
509
577
  network_split=config.network_split,
510
578
  reverse_proxy=config.reverse_proxy,
511
- database_kind=config.database.kind if config.database else None,
512
- services=tuple(config.services),
579
+ database_kind=carried_db.service if carried_db is not None else None,
580
+ services=representable,
513
581
  redundant_role=redundant_role,
514
582
  disable_builtins=disable_builtins,
583
+ iiot=iiot_broker is not None,
584
+ iiot_broker=iiot_broker,
515
585
  )
516
586
 
517
587
 
@@ -100,8 +100,7 @@ def validate(
100
100
  raise typer.Exit(code=1)
101
101
 
102
102
  console.print(
103
- f"[green]OK[/green]: {len(catalog.entries)} entries valid"
104
- + (" (schema only)" if skip_network else " (schema + reachability)"),
103
+ f"[green]OK[/green]: {len(catalog.entries)} entries valid" + (" (schema only)" if skip_network else " (schema + reachability)"),
105
104
  )
106
105
 
107
106
 
@@ -44,6 +44,22 @@ def complete_reverse_proxy(incomplete: str) -> list[str]:
44
44
  return [kind for kind in REVERSE_PROXY_VALUES if kind.startswith(incomplete)]
45
45
 
46
46
 
47
+ def complete_iiot_broker(incomplete: str) -> list[tuple[str, str]]:
48
+ """MQTT broker slugs (with summary) the IIoT overlay can wire to.
49
+
50
+ Reads the bundled service catalog and offers only ``mqtt-broker`` kinds, the
51
+ slugs ``--iiot-broker`` accepts. Degrades to no suggestions on any error so a
52
+ TAB never breaks the shell line.
53
+ """
54
+ try:
55
+ from ignition_stack.services.loader import load_all_services
56
+
57
+ catalog = load_all_services()
58
+ except Exception:
59
+ return []
60
+ return [(slug, m.summary) for slug, m in sorted(catalog.items()) if m.kind == "mqtt-broker" and slug.startswith(incomplete)]
61
+
62
+
47
63
  # Roles `init --redundant` can pair. Only the singleton workhorse roles are
48
64
  # eligible (a scaleout 'backend', a hub-and-spoke 'hub', a standalone
49
65
  # 'gateway'); replicated 'frontend'/'spoke' tiers are rejected by the profile