ignition-stack 0.4.0__tar.gz → 0.6.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 (208) hide show
  1. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/.gitignore +3 -0
  2. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/PKG-INFO +7 -5
  3. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/README.md +6 -4
  4. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/builtin_modules.yaml +38 -0
  5. ignition_stack-0.6.0/ignition_stack/__init__.py +1 -0
  6. ignition_stack-0.6.0/ignition_stack/architectures/__init__.py +35 -0
  7. {ignition_stack-0.4.0/ignition_stack/profiles → ignition_stack-0.6.0/ignition_stack/architectures}/advisory.py +1 -1
  8. ignition_stack-0.6.0/ignition_stack/architectures/base.py +344 -0
  9. ignition_stack-0.6.0/ignition_stack/architectures/basic.py +45 -0
  10. ignition_stack-0.6.0/ignition_stack/architectures/carry.py +319 -0
  11. {ignition_stack-0.4.0/ignition_stack/profiles → ignition_stack-0.6.0/ignition_stack/architectures}/hub_and_spoke.py +23 -23
  12. ignition_stack-0.4.0/ignition_stack/profiles/scaleout.py → ignition_stack-0.6.0/ignition_stack/architectures/scale_out.py +18 -17
  13. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/catalog/builtins.py +47 -11
  14. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/catalog/download.py +4 -14
  15. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/catalog/schema.py +5 -18
  16. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/cli.py +185 -109
  17. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/commands/modules.py +1 -2
  18. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/completion.py +37 -20
  19. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/compose/engine.py +185 -46
  20. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/compose/templates/footer.yaml.j2 +5 -1
  21. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/compose/templates/services/ignition.yaml.j2 +15 -3
  22. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/compose/writer.py +216 -53
  23. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/config/__init__.py +4 -0
  24. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/config/io.py +3 -8
  25. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/config/schema.py +342 -27
  26. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/lifecycle/record.py +3 -3
  27. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/lifecycle/regenerate.py +1 -1
  28. ignition_stack-0.6.0/ignition_stack/postsetup/generator.py +476 -0
  29. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/services/loader.py +1 -3
  30. ignition_stack-0.6.0/ignition_stack/services/manifest.py +263 -0
  31. ignition_stack-0.6.0/ignition_stack/services/resolver.py +388 -0
  32. ignition_stack-0.6.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
  33. ignition_stack-0.6.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
  34. ignition_stack-0.6.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/general/config.json +21 -0
  35. ignition_stack-0.6.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/general/resource.json +16 -0
  36. ignition_stack-0.6.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
  37. ignition_stack-0.6.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
  38. ignition_stack-0.6.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
  39. ignition_stack-0.6.0/ignition_stack/templates/iiot/gateway-resources-mqtt-engine/config/resources/core/com.cirruslink.mqtt.engine.gateway/server/Chariot SCADA/resource.json +19 -0
  40. ignition_stack-0.6.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
  41. ignition_stack-0.6.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
  42. ignition_stack-0.6.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/general/config.json +4 -0
  43. ignition_stack-0.6.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/general/resource.json +16 -0
  44. ignition_stack-0.6.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
  45. ignition_stack-0.6.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
  46. ignition_stack-0.6.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
  47. ignition_stack-0.6.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/server/Chariot SCADA/resource.json +19 -0
  48. ignition_stack-0.6.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/server-set/Default/config.json +5 -0
  49. ignition_stack-0.6.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/server-set/Default/resource.json +17 -0
  50. ignition_stack-0.6.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/transmitter/{{gateway}}/config.json.j2 +29 -0
  51. ignition_stack-0.6.0/ignition_stack/templates/iiot/gateway-resources-mqtt-transmission/config/resources/core/com.cirruslink.mqtt.transmission.gateway/transmitter/{{gateway}}/resource.json +14 -0
  52. ignition_stack-0.6.0/ignition_stack/templates/post-setup/connections.md.j2 +23 -0
  53. ignition_stack-0.6.0/ignition_stack/templates/post-setup/identity-provider.md.j2 +25 -0
  54. ignition_stack-0.6.0/ignition_stack/templates/post-setup/mqtt-engine-connection.md.j2 +106 -0
  55. ignition_stack-0.6.0/ignition_stack/templates/post-setup/reverse-proxy.md.j2 +17 -0
  56. ignition_stack-0.6.0/ignition_stack/templates/services/chariot/compose.yaml.j2 +39 -0
  57. ignition_stack-0.6.0/ignition_stack/templates/services/chariot/manifest.yaml +48 -0
  58. ignition_stack-0.6.0/ignition_stack/templates/services/chariot/seed/service/chariot-trial.sh +56 -0
  59. ignition_stack-0.6.0/ignition_stack/templates/services/emqx/manifest.yaml +39 -0
  60. ignition_stack-0.6.0/ignition_stack/templates/services/hivemq/manifest.yaml +35 -0
  61. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/kafka/manifest.yaml +8 -1
  62. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/keycloak/compose.yaml.j2 +2 -0
  63. ignition_stack-0.6.0/ignition_stack/templates/services/keycloak/manifest.yaml +41 -0
  64. ignition_stack-0.6.0/ignition_stack/templates/services/keycloak/seed/gateway-resources/config/resources/core/ignition/identity-provider/keycloak/config.json +57 -0
  65. ignition_stack-0.6.0/ignition_stack/templates/services/keycloak/seed/gateway-resources/config/resources/core/ignition/identity-provider/keycloak/resource.json +18 -0
  66. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/keycloak/seed/service/import/ignition-realm.json +23 -2
  67. ignition_stack-0.6.0/ignition_stack/templates/services/mariadb/manifest.yaml +28 -0
  68. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/modbus-sim/manifest.yaml +6 -1
  69. ignition_stack-0.6.0/ignition_stack/templates/services/mongo/manifest.yaml +27 -0
  70. ignition_stack-0.6.0/ignition_stack/templates/services/mysql/manifest.yaml +28 -0
  71. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/n8n/manifest.yaml +6 -1
  72. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/opcua-sim/manifest.yaml +6 -1
  73. ignition_stack-0.6.0/ignition_stack/templates/services/postgres/manifest.yaml +39 -0
  74. ignition_stack-0.6.0/ignition_stack/templates/services/rabbitmq/manifest.yaml +41 -0
  75. ignition_stack-0.6.0/ignition_stack/wizard.py +1054 -0
  76. ignition_stack-0.6.0/ignition_stack/wizard_composer.py +716 -0
  77. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/modules.yaml +7 -7
  78. {ignition_stack-0.4.0/tests/golden/profiles → ignition_stack-0.6.0/tests/golden/architectures}/hub-and-spoke/docker-compose.yaml +0 -6
  79. ignition_stack-0.6.0/tests/golden/combos/hub-and-spoke-iiot-chariot/docker-compose.yaml +184 -0
  80. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/combos/network-split/docker-compose.yaml +2 -0
  81. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/scaleout-skeleton/docker-compose.yaml +0 -2
  82. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/chariot/docker-compose.yaml +16 -0
  83. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/keycloak/docker-compose.yaml +2 -2
  84. ignition_stack-0.6.0/tests/test_architectures.py +1167 -0
  85. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_completion.py +8 -8
  86. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_compose_engine.py +25 -23
  87. ignition_stack-0.6.0/tests/test_declarative_io.py +379 -0
  88. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_disable_builtins.py +80 -20
  89. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_docs_cli_reference.py +6 -9
  90. ignition_stack-0.6.0/tests/test_iiot_overlay.py +384 -0
  91. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_init_standalone.py +9 -14
  92. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_lifecycle.py +15 -17
  93. ignition_stack-0.6.0/tests/test_manifest_invariants.py +348 -0
  94. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_modules_catalog.py +1 -3
  95. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_modules_cli.py +1 -3
  96. ignition_stack-0.6.0/tests/test_postsetup.py +279 -0
  97. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_redundancy.py +22 -33
  98. ignition_stack-0.6.0/tests/test_registry.py +291 -0
  99. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_service_catalog.py +117 -35
  100. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_service_catalog_smoke.py +1 -3
  101. ignition_stack-0.6.0/tests/test_switch_arch_registry.py +345 -0
  102. ignition_stack-0.6.0/tests/test_wizard_back_nav.py +338 -0
  103. ignition_stack-0.6.0/tests/test_wizard_breadcrumb.py +188 -0
  104. ignition_stack-0.6.0/tests/test_wizard_composer.py +747 -0
  105. ignition_stack-0.6.0/verification/iiot-spike/README.md +142 -0
  106. ignition_stack-0.4.0/ignition_stack/__init__.py +0 -1
  107. ignition_stack-0.4.0/ignition_stack/postsetup/generator.py +0 -245
  108. ignition_stack-0.4.0/ignition_stack/profiles/__init__.py +0 -31
  109. ignition_stack-0.4.0/ignition_stack/profiles/base.py +0 -236
  110. ignition_stack-0.4.0/ignition_stack/profiles/mcp_n8n.py +0 -55
  111. ignition_stack-0.4.0/ignition_stack/profiles/standalone.py +0 -44
  112. ignition_stack-0.4.0/ignition_stack/services/manifest.py +0 -106
  113. ignition_stack-0.4.0/ignition_stack/services/resolver.py +0 -186
  114. ignition_stack-0.4.0/ignition_stack/templates/post-setup/identity-provider.md.j2 +0 -13
  115. ignition_stack-0.4.0/ignition_stack/templates/post-setup/mqtt-engine-connection.md.j2 +0 -11
  116. ignition_stack-0.4.0/ignition_stack/templates/post-setup/reverse-proxy.md.j2 +0 -8
  117. ignition_stack-0.4.0/ignition_stack/templates/services/chariot/compose.yaml.j2 +0 -17
  118. ignition_stack-0.4.0/ignition_stack/templates/services/chariot/manifest.yaml +0 -22
  119. ignition_stack-0.4.0/ignition_stack/templates/services/emqx/manifest.yaml +0 -21
  120. ignition_stack-0.4.0/ignition_stack/templates/services/hivemq/manifest.yaml +0 -19
  121. ignition_stack-0.4.0/ignition_stack/templates/services/keycloak/manifest.yaml +0 -25
  122. ignition_stack-0.4.0/ignition_stack/templates/services/mariadb/manifest.yaml +0 -15
  123. ignition_stack-0.4.0/ignition_stack/templates/services/mongo/manifest.yaml +0 -14
  124. ignition_stack-0.4.0/ignition_stack/templates/services/mysql/manifest.yaml +0 -15
  125. ignition_stack-0.4.0/ignition_stack/templates/services/postgres/manifest.yaml +0 -21
  126. ignition_stack-0.4.0/ignition_stack/templates/services/rabbitmq/manifest.yaml +0 -23
  127. ignition_stack-0.4.0/ignition_stack/wizard.py +0 -472
  128. ignition_stack-0.4.0/tests/golden/profiles/mcp-n8n/docker-compose.yaml +0 -72
  129. ignition_stack-0.4.0/tests/test_declarative_io.py +0 -193
  130. ignition_stack-0.4.0/tests/test_postsetup.py +0 -84
  131. ignition_stack-0.4.0/tests/test_profiles.py +0 -844
  132. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/LICENSE +0 -0
  133. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/catalog/__init__.py +0 -0
  134. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/catalog/loader.py +0 -0
  135. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/catalog/verify.py +0 -0
  136. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/commands/__init__.py +0 -0
  137. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/compose/__init__.py +0 -0
  138. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/compose/templates/header.yaml.j2 +0 -0
  139. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/compose/templates/services/bootstrap.yaml.j2 +0 -0
  140. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/lifecycle/__init__.py +0 -0
  141. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/lifecycle/cleanup.py +0 -0
  142. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/postsetup/__init__.py +0 -0
  143. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/services/__init__.py +0 -0
  144. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/__init__.py +0 -0
  145. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/post-setup/_default.md.j2 +0 -0
  146. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/post-setup/device-connection.md.j2 +0 -0
  147. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/post-setup/gateway-network-link.md.j2 +0 -0
  148. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/post-setup/kafka-connector.md.j2 +0 -0
  149. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/post-setup/mcp-module.md.j2 +0 -0
  150. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/post-setup/opc-ua-connection.md.j2 +0 -0
  151. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/post-setup/redundancy-pairing.md.j2 +0 -0
  152. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/redundancy/redundancy.xml.j2 +0 -0
  153. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/chariot/seed/service/USAGE.md +0 -0
  154. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/emqx/compose.yaml.j2 +0 -0
  155. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/emqx/seed/service/USAGE.md +0 -0
  156. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/hivemq/compose.yaml.j2 +0 -0
  157. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/hivemq/seed/service/USAGE.md +0 -0
  158. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/kafka/compose.yaml.j2 +0 -0
  159. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/kafka/seed/service/USAGE.md +0 -0
  160. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/mariadb/compose.yaml.j2 +0 -0
  161. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/mariadb/seed/service/initdb/00-create-extra-databases.sh +0 -0
  162. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/modbus-sim/compose.yaml.j2 +0 -0
  163. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/modbus-sim/seed/service/USAGE.md +0 -0
  164. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/mongo/compose.yaml.j2 +0 -0
  165. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/mongo/seed/service/initdb/01-demo-collection.js +0 -0
  166. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/mysql/compose.yaml.j2 +0 -0
  167. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/mysql/seed/service/initdb/00-create-extra-databases.sh +0 -0
  168. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/n8n/compose.yaml.j2 +0 -0
  169. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/n8n/seed/service/USAGE.md +0 -0
  170. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/opcua-sim/compose.yaml.j2 +0 -0
  171. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/opcua-sim/seed/service/USAGE.md +0 -0
  172. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/postgres/compose.yaml.j2 +0 -0
  173. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/config.json +0 -0
  174. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/resource.json +0 -0
  175. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/secret-provider/internal-secret-provider/config.json +0 -0
  176. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/secret-provider/internal-secret-provider/resource.json +0 -0
  177. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/postgres/seed/service/initdb/00-create-extra-databases.sh +0 -0
  178. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/rabbitmq/compose.yaml.j2 +0 -0
  179. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/services/rabbitmq/seed/service/enabled_plugins +0 -0
  180. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/standalone-postgres/docker-compose.yaml +0 -0
  181. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/standalone-postgres/scripts/docker-bootstrap.sh +0 -0
  182. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/standalone-postgres/services/ignition/config/resources/core/config-mode.json +0 -0
  183. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/standalone-postgres/services/ignition/config/resources/dev/config-mode.json +0 -0
  184. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/templates/standalone-postgres/services/ignition/projects/.gitkeep +0 -0
  185. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/ignition_stack/update_check.py +0 -0
  186. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/pyproject.toml +0 -0
  187. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/__init__.py +0 -0
  188. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/conftest.py +0 -0
  189. {ignition_stack-0.4.0/tests/golden/profiles/standalone → ignition_stack-0.6.0/tests/golden/architectures/basic}/docker-compose.yaml +0 -0
  190. {ignition_stack-0.4.0/tests/golden/profiles/scaleout → ignition_stack-0.6.0/tests/golden/architectures/scale-out}/docker-compose.yaml +0 -0
  191. {ignition_stack-0.4.0/tests/golden/profiles/scaleout-redundant → ignition_stack-0.6.0/tests/golden/architectures/scale-out-redundant}/docker-compose.yaml +0 -0
  192. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/combos/smoke-stack/docker-compose.yaml +0 -0
  193. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/db-mariadb/docker-compose.yaml +0 -0
  194. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/db-mongo/docker-compose.yaml +0 -0
  195. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/db-mysql/docker-compose.yaml +0 -0
  196. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/db-postgres/docker-compose.yaml +0 -0
  197. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/emqx/docker-compose.yaml +0 -0
  198. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/hivemq/docker-compose.yaml +0 -0
  199. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/kafka/docker-compose.yaml +0 -0
  200. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/modbus-sim/docker-compose.yaml +0 -0
  201. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/n8n/docker-compose.yaml +0 -0
  202. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/opcua-sim/docker-compose.yaml +0 -0
  203. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/services/rabbitmq/docker-compose.yaml +0 -0
  204. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/golden/standalone-postgres/docker-compose.yaml +0 -0
  205. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_builtin_catalog_smoke.py +0 -0
  206. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/tests/test_update_check.py +0 -0
  207. {ignition_stack-0.4.0 → ignition_stack-0.6.0}/verification/redundancy-spike/README.md +0 -0
  208. {ignition_stack-0.4.0 → ignition_stack-0.6.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.4.0
3
+ Version: 0.6.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
@@ -33,7 +33,7 @@ Description-Content-Type: text/markdown
33
33
  [![Docs](https://github.com/ia-eknorr/ignition-stack/actions/workflows/docs.yml/badge.svg)](https://github.com/ia-eknorr/ignition-stack/actions/workflows/docs.yml)
34
34
  [![Documentation](https://img.shields.io/badge/docs-ia--eknorr.github.io-blue)](https://ia-eknorr.github.io/ignition-stack/)
35
35
 
36
- CLI that generates ready-to-run Docker Compose stacks for Ignition 8.3 SCADA demos and SE engagements. Picks an architecture profile, asks a few questions, writes a self-contained project with a hand-readable compose file, env, file-config seed resources, and a `POST-SETUP.md` listing only what could not be pre-seeded.
36
+ CLI that generates ready-to-run Docker Compose stacks for Ignition 8.3 SCADA demos and SE engagements. Pick a system architecture (`basic`, `scale-out`, or `hub-and-spoke`), layer on services, and it writes a self-contained project: a hand-readable compose file, `.env`, file-config seed resources, and a `POST-SETUP.md` listing only what could not be pre-seeded.
37
37
 
38
38
  See [`docs/docs/reference/seeding-matrix.md`](docs/docs/reference/seeding-matrix.md) for which Ignition 8.3 connection types can be provisioned from the filesystem and env on a live 8.3.6 gateway. Full documentation lives in the [`docs/`](docs/) Docusaurus site.
39
39
 
@@ -55,23 +55,25 @@ pipx install git+https://github.com/ia-eknorr/ignition-stack.git@<branch>
55
55
  Generate a project and bring it up:
56
56
 
57
57
  ```sh
58
- ignition-stack init demo
58
+ ignition-stack init demo --arch basic
59
59
  cd demo
60
60
  docker compose up -d
61
61
  ```
62
62
 
63
63
  The gateway reaches RUNNING with no UI prompts. The admin user is `admin / password` and the gateway is at `http://localhost:9088`. The default Postgres credentials are `ignition / ignition` on the `db` service.
64
64
 
65
+ Run `init` without `--arch` to walk the interactive wizard instead: it opens architecture-first, then layers database, edition, IIoT, services, and exposure on top, with a summary you can preview, tweak, or generate.
66
+
65
67
  Everything that ships in the generated project is hand-readable: `docker-compose.yaml`, `.env`, `scripts/docker-bootstrap.sh`, and a `services/ignition/` resources tree the gateway reads on first boot.
66
68
 
67
69
  ## Commands
68
70
 
69
71
  | Command | What it does |
70
72
  | --- | --- |
71
- | `init <name>` | Generate a project at `./<name>/` from a profile and a few prompts. |
73
+ | `init <name>` | Generate a project at `./<name>/` from an architecture and a few prompts. |
72
74
  | `modules` | Download, verify, and manage the `.modl` / JDBC catalog. |
73
75
  | `reset` | Re-run generation from an SE-demo project's recorded config. |
74
- | `switch-profile` | Reshape an SE-demo project under a different profile. |
76
+ | `switch-arch` | Reshape an SE-demo project under a different architecture. |
75
77
  | `wipe` | Remove this project's containers and volumes only. |
76
78
 
77
79
  See the [CLI reference](docs/docs/reference/cli.md) for every command, argument, and option.
@@ -4,7 +4,7 @@
4
4
  [![Docs](https://github.com/ia-eknorr/ignition-stack/actions/workflows/docs.yml/badge.svg)](https://github.com/ia-eknorr/ignition-stack/actions/workflows/docs.yml)
5
5
  [![Documentation](https://img.shields.io/badge/docs-ia--eknorr.github.io-blue)](https://ia-eknorr.github.io/ignition-stack/)
6
6
 
7
- CLI that generates ready-to-run Docker Compose stacks for Ignition 8.3 SCADA demos and SE engagements. Picks an architecture profile, asks a few questions, writes a self-contained project with a hand-readable compose file, env, file-config seed resources, and a `POST-SETUP.md` listing only what could not be pre-seeded.
7
+ CLI that generates ready-to-run Docker Compose stacks for Ignition 8.3 SCADA demos and SE engagements. Pick a system architecture (`basic`, `scale-out`, or `hub-and-spoke`), layer on services, and it writes a self-contained project: a hand-readable compose file, `.env`, file-config seed resources, and a `POST-SETUP.md` listing only what could not be pre-seeded.
8
8
 
9
9
  See [`docs/docs/reference/seeding-matrix.md`](docs/docs/reference/seeding-matrix.md) for which Ignition 8.3 connection types can be provisioned from the filesystem and env on a live 8.3.6 gateway. Full documentation lives in the [`docs/`](docs/) Docusaurus site.
10
10
 
@@ -26,23 +26,25 @@ pipx install git+https://github.com/ia-eknorr/ignition-stack.git@<branch>
26
26
  Generate a project and bring it up:
27
27
 
28
28
  ```sh
29
- ignition-stack init demo
29
+ ignition-stack init demo --arch basic
30
30
  cd demo
31
31
  docker compose up -d
32
32
  ```
33
33
 
34
34
  The gateway reaches RUNNING with no UI prompts. The admin user is `admin / password` and the gateway is at `http://localhost:9088`. The default Postgres credentials are `ignition / ignition` on the `db` service.
35
35
 
36
+ Run `init` without `--arch` to walk the interactive wizard instead: it opens architecture-first, then layers database, edition, IIoT, services, and exposure on top, with a summary you can preview, tweak, or generate.
37
+
36
38
  Everything that ships in the generated project is hand-readable: `docker-compose.yaml`, `.env`, `scripts/docker-bootstrap.sh`, and a `services/ignition/` resources tree the gateway reads on first boot.
37
39
 
38
40
  ## Commands
39
41
 
40
42
  | Command | What it does |
41
43
  | --- | --- |
42
- | `init <name>` | Generate a project at `./<name>/` from a profile and a few prompts. |
44
+ | `init <name>` | Generate a project at `./<name>/` from an architecture and a few prompts. |
43
45
  | `modules` | Download, verify, and manage the `.modl` / JDBC catalog. |
44
46
  | `reset` | Re-run generation from an SE-demo project's recorded config. |
45
- | `switch-profile` | Reshape an SE-demo project under a different profile. |
47
+ | `switch-arch` | Reshape an SE-demo project under a different architecture. |
46
48
  | `wipe` | Remove this project's containers and volumes only. |
47
49
 
48
50
  See the [CLI reference](docs/docs/reference/cli.md) for every command, argument, and option.
@@ -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.6.0"
@@ -0,0 +1,35 @@
1
+ """System architectures: pre-canned shapes that turn intent into config.
2
+
3
+ The slugs mirror Ignition's documented system architectures (Basic / Scale Out
4
+ / Hub and Spoke). Importing this package registers every built-in architecture
5
+ by side-effect so ``get_architecture("scale-out")`` works without explicit
6
+ module imports.
7
+ """
8
+
9
+ from ignition_stack.architectures import basic, hub_and_spoke, scale_out # noqa: F401
10
+ from ignition_stack.architectures.advisory import Advisory, spoke_advisory
11
+ from ignition_stack.architectures.base import (
12
+ Architecture,
13
+ ArchOptions,
14
+ apply_iiot,
15
+ build_architecture,
16
+ can_host_redundant_role,
17
+ get_architecture,
18
+ list_architectures,
19
+ mark_redundant,
20
+ )
21
+ from ignition_stack.architectures.hub_and_spoke import ArchitectureError
22
+
23
+ __all__ = [
24
+ "Advisory",
25
+ "ArchOptions",
26
+ "Architecture",
27
+ "ArchitectureError",
28
+ "apply_iiot",
29
+ "build_architecture",
30
+ "can_host_redundant_role",
31
+ "get_architecture",
32
+ "list_architectures",
33
+ "mark_redundant",
34
+ "spoke_advisory",
35
+ ]
@@ -1,6 +1,6 @@
1
1
  """Hub-and-spoke RAM advisory.
2
2
 
3
- The hub-and-spoke profile spins up one hub gateway plus *N* spokes; each
3
+ The hub-and-spoke architecture spins up one hub gateway plus *N* spokes; each
4
4
  Ignition gateway needs ~1.5 GB to run comfortably. The advisory turns that
5
5
  math into a proportional friction signal so SEs can see the cost of a
6
6
  large demo without being silently blocked when they really do want it:
@@ -0,0 +1,344 @@
1
+ """Architecture contract + options + registry.
2
+
3
+ An *architecture* is the small piece of code that turns the user's high-level
4
+ intent ("scale-out", "hub-and-spoke with 3 spokes") into a fully-formed
5
+ :class:`ProjectConfig`. The slugs mirror Ignition's own documented system
6
+ architectures (Basic / Scale Out / Hub and Spoke). The compose engine and the
7
+ dependency resolver are architecture-agnostic; architectures only shape the
8
+ inputs they take.
9
+
10
+ Two-stage pipeline:
11
+
12
+ 1. Either the CLI flags or the wizard answers populate an
13
+ :class:`ArchOptions` and pick an architecture slug.
14
+ 2. ``build_architecture(slug, name, options)`` looks up the architecture and
15
+ calls its ``build()`` method, returning a ``ProjectConfig`` that
16
+ ``services.resolver.resolve()`` then expands the usual implicit deps on.
17
+
18
+ Each architecture is a small dataclass with three pieces:
19
+
20
+ - ``slug`` - the wizard/flag value users type.
21
+ - ``summary`` - one-line description for the wizard menu + docs.
22
+ - ``build`` - pure function ``(name, options) -> ProjectConfig``.
23
+
24
+ Keeping ``build`` pure (no I/O, no prompts) is what lets the wizard layer
25
+ and the CLI flag layer share the same code path and stay testable.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from dataclasses import dataclass
31
+ from typing import Protocol
32
+
33
+ from ignition_stack.config import (
34
+ GatewayConfig,
35
+ ProjectConfig,
36
+ RedundancyConfig,
37
+ ReverseProxyConfig,
38
+ ServiceAttachment,
39
+ ServiceInstance,
40
+ )
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class ArchOptions:
45
+ """Inputs each architecture reads to shape the resolved config.
46
+
47
+ Every field has a sensible default so callers only set what they
48
+ actually care about. The wizard fills in many of these from prompts;
49
+ the non-interactive CLI path fills in a subset from flags and leaves
50
+ the rest at their defaults.
51
+ """
52
+
53
+ spokes: int = 3
54
+ """Hub-and-spoke spoke count. Ignored by other architectures."""
55
+
56
+ frontends: int = 1
57
+ """Scale-out frontend gateway count. Ignored by other architectures.
58
+
59
+ 1 yields a single gateway named ``frontend``; N>1 yields
60
+ ``frontend-1``..``frontend-N``. A ``backend`` gateway is always added
61
+ on top.
62
+ """
63
+
64
+ force: bool = False
65
+ """Bypass the hub-and-spoke red-tier advisory. Ignored elsewhere."""
66
+
67
+ network_split: bool | None = None
68
+ """Tri-state override for the frontend/backend network split.
69
+
70
+ ``None`` lets each architecture apply its own default (scale-out splits,
71
+ hub-and-spoke does not). ``True``/``False`` force the split on or off
72
+ regardless of the architecture default.
73
+ """
74
+
75
+ edge_role: str | None = None
76
+ """Which gateway role (if any) runs the Edge edition.
77
+
78
+ For scale-out this is typically 'frontend'; for hub-and-spoke it can
79
+ be 'spoke' (every spoke runs Edge) or None. The architecture is free to
80
+ apply its own default when this is None.
81
+ """
82
+
83
+ reverse_proxy: ReverseProxyConfig | None = None
84
+ """Reverse-proxy scaffolding. None = plain host-port mapping."""
85
+
86
+ database_kind: str | None = "postgres"
87
+ """SQL database for the stack. None = no database (gateway-only)."""
88
+
89
+ services: tuple[str, ...] = ()
90
+ """Additional service catalog slugs the user picked beyond architecture defaults."""
91
+
92
+ redundant_role: str | None = None
93
+ """Role (or gateway name) to make redundant, expanding it into a master +
94
+ backup pair. ``None`` (default) builds no redundancy. Must name a singleton
95
+ workhorse role (scale-out 'backend', hub-and-spoke 'hub', basic
96
+ 'gateway'); replicated tiers ('frontend', 'spoke') are rejected."""
97
+
98
+ disable_builtins: tuple[str, ...] = ()
99
+ """Built-in module slugs to turn off on every gateway in the stack.
100
+
101
+ Empty (default) leaves all built-ins on. Applied uniformly by
102
+ ``build_architecture`` - the demo intent is "drop Vision/SFC everywhere",
103
+ and per-gateway disabling stays a declarative-config-only feature. Slugs
104
+ are validated against builtin_modules.yaml by ``GatewayConfig``."""
105
+
106
+ iiot: bool = False
107
+ """Overlay an MQTT/Sparkplug IIoT pipeline (a broker + Cirrus Link
108
+ Transmission/Engine) onto the stack. Off by default; ``build_architecture``
109
+ calls :func:`apply_iiot` when this is set, defaulting the broker to
110
+ ``chariot``."""
111
+
112
+ iiot_broker: str | None = None
113
+ """MQTT broker slug the IIoT overlay wires to. ``None`` with ``iiot`` on
114
+ means the confirmed default ``chariot`` (Cirrus Link's own broker). Ignored
115
+ when ``iiot`` is False. Validated against the catalog by :func:`apply_iiot`."""
116
+
117
+
118
+ class Architecture(Protocol):
119
+ """A factory that turns ``ArchOptions`` into a ``ProjectConfig``."""
120
+
121
+ slug: str
122
+ summary: str
123
+
124
+ def build(self, name: str, options: ArchOptions) -> ProjectConfig: ...
125
+
126
+
127
+ # Registry populated by the architecture modules at import time. Keep
128
+ # alphabetical insertion-order for stable wizard menus + --help listings.
129
+ _REGISTRY: dict[str, Architecture] = {}
130
+
131
+
132
+ def register(architecture: Architecture) -> Architecture:
133
+ """Register an architecture by slug. Returns the architecture so module-level
134
+ uses can write ``basic = register(BasicArchitecture())``.
135
+ """
136
+ if architecture.slug in _REGISTRY:
137
+ raise ValueError(f"architecture '{architecture.slug}' is already registered")
138
+ _REGISTRY[architecture.slug] = architecture
139
+ return architecture
140
+
141
+
142
+ def get_architecture(slug: str) -> Architecture:
143
+ """Look up a registered architecture by slug. Raises ``KeyError`` if unknown."""
144
+ try:
145
+ return _REGISTRY[slug]
146
+ except KeyError as exc:
147
+ known = ", ".join(sorted(_REGISTRY))
148
+ raise KeyError(f"unknown architecture '{slug}'; known architectures: {known}") from exc
149
+
150
+
151
+ def list_architectures() -> list[Architecture]:
152
+ """All registered architectures in stable insertion order."""
153
+ return list(_REGISTRY.values())
154
+
155
+
156
+ # Roles that are horizontally replicated, not paired. Ignition redundancy is a
157
+ # two-node master/backup arrangement, so a tier that can have many members
158
+ # (frontends, spokes) is never the redundancy target - those scale out, they
159
+ # don't fail over. Marking one redundant is a usage error.
160
+ _NON_REDUNDANT_ROLES = frozenset({"frontend", "spoke"})
161
+
162
+
163
+ def _matching_gateways(config: ProjectConfig, redundant_role: str) -> list:
164
+ """Gateways a redundant role names, matched by role or name.
165
+
166
+ The single matching rule shared by :func:`mark_redundant` (which errors on a
167
+ bad match) and :func:`can_host_redundant_role` (which only reports), so the
168
+ two can never drift on what "matches" means.
169
+ """
170
+ return [gw for gw in config.gateways if redundant_role in (gw.role, gw.name)]
171
+
172
+
173
+ def can_host_redundant_role(config: ProjectConfig, redundant_role: str) -> bool:
174
+ """True when ``config`` has exactly one gateway that can be paired as master.
175
+
176
+ ``switch-arch`` uses this to decide whether redundancy intent recovered from
177
+ the old stack can carry to the target architecture, without raising the way
178
+ :func:`mark_redundant` does - an architecture-specific role (e.g. basic's
179
+ ``gateway``) simply may not exist in the destination.
180
+ """
181
+ if redundant_role in _NON_REDUNDANT_ROLES:
182
+ return False
183
+ return len(_matching_gateways(config, redundant_role)) == 1
184
+
185
+
186
+ def mark_redundant(config: ProjectConfig, redundant_role: str | None) -> ProjectConfig:
187
+ """Stamp the gateway named/roled ``redundant_role`` as a redundancy master.
188
+
189
+ Returns ``config`` unchanged when ``redundant_role`` is None. The expansion
190
+ into a master+backup pair happens later in
191
+ :func:`ignition_stack.services.resolver.resolve`; this only marks which
192
+ gateway to pair, so the same logic serves every architecture and the wizard.
193
+
194
+ Raises ``ValueError`` (surfaced by the CLI as a usage error, exit code 2)
195
+ when the role is a replicated frontend/spoke tier, unknown, or ambiguous
196
+ (matches more than one gateway - you can't pair a horizontally-scaled tier).
197
+ """
198
+ if redundant_role is None:
199
+ return config
200
+ if redundant_role in _NON_REDUNDANT_ROLES:
201
+ raise ValueError(
202
+ f"role '{redundant_role}' is horizontally replicated, not paired; "
203
+ "redundancy applies to a single gateway (e.g. a scale-out 'backend' "
204
+ "or a hub-and-spoke 'hub'), never to frontends or spokes"
205
+ )
206
+ matches = _matching_gateways(config, redundant_role)
207
+ if not matches:
208
+ known = ", ".join(sorted({gw.role or gw.name for gw in config.gateways}))
209
+ raise ValueError(f"no gateway matches redundant role '{redundant_role}'; available roles: {known}")
210
+ if len(matches) > 1:
211
+ raise ValueError(f"redundant role '{redundant_role}' matches {len(matches)} gateways; " "redundancy pairs a single gateway, so name a singleton role")
212
+ master = matches[0]
213
+ master.redundancy = RedundancyConfig(mode="master", peer=f"{master.name}-backup")
214
+ return config
215
+
216
+
217
+ def build_architecture(slug: str, name: str, options: ArchOptions) -> ProjectConfig:
218
+ """Materialize a ``ProjectConfig`` for the named architecture.
219
+
220
+ The architecture builds the base topology; ``mark_redundant`` then stamps
221
+ the redundancy master when ``options.redundant_role`` is set, leaving the
222
+ resolver to expand the pair. Keeping the stamp here (not in each
223
+ architecture) means one eligibility rule serves every architecture and the
224
+ wizard alike.
225
+ """
226
+ config = get_architecture(slug).build(name, options)
227
+ config = mark_redundant(config, options.redundant_role)
228
+ config = apply_disable_builtins(config, options.disable_builtins)
229
+ broker = (options.iiot_broker or _IIOT_DEFAULT_BROKER) if options.iiot else None
230
+ return apply_iiot(config, broker)
231
+
232
+
233
+ def apply_disable_builtins(config: ProjectConfig, disable_builtins: tuple[str, ...]) -> ProjectConfig:
234
+ """Stamp ``disable_builtins`` onto every gateway in ``config``.
235
+
236
+ Applied centrally (like :func:`mark_redundant`) so one rule serves every
237
+ architecture and the wizard. Uniform across gateways: the demo intent is to
238
+ drop a module everywhere, and a redundant pair must agree on its module set.
239
+ The resolver later copies the list onto any expanded backup node.
240
+ """
241
+ if not disable_builtins:
242
+ return config
243
+ # pydantic does not re-validate on attribute assignment, so validate here -
244
+ # this is the wizard/CLI choke point (the declarative path is checked at
245
+ # construction). Raises ValueError on an unknown slug; the CLI maps that to
246
+ # exit code 2.
247
+ from ignition_stack.catalog.builtins import validate_disable_slugs
248
+
249
+ validate_disable_slugs(list(disable_builtins))
250
+ for gw in config.gateways:
251
+ gw.disable_builtins = list(disable_builtins)
252
+ return config
253
+
254
+
255
+ # The confirmed default IIoT broker (Cirrus Link's own Chariot, the most
256
+ # official pairing with their Transmission/Engine modules). Used when the
257
+ # overlay is requested without an explicit broker slug.
258
+ _IIOT_DEFAULT_BROKER = "chariot"
259
+
260
+ # Gateway roles that run MQTT Transmission (edge-side: publish Sparkplug to the
261
+ # broker) versus MQTT Engine (central: subscribe and aggregate). A basic shape
262
+ # has neither role, so its single gateway runs both for a self-contained demo
263
+ # loop through the broker.
264
+ _TRANSMISSION_ROLES = frozenset({"spoke", "frontend"})
265
+ _ENGINE_ROLES = frozenset({"hub", "backend"})
266
+
267
+
268
+ def apply_iiot(config: ProjectConfig, broker: str | None) -> ProjectConfig:
269
+ """Overlay an MQTT/Sparkplug IIoT pipeline onto ``config``.
270
+
271
+ Returns ``config`` unchanged when ``broker`` is None. Otherwise it adds the
272
+ broker as a stack-level :class:`ServiceInstance` (singleton, enforced by the
273
+ resolver) and wires each gateway by role, using the **module slugs the broker
274
+ manifest's ``wires.mqtt`` block names** (never hardcoded):
275
+
276
+ - ``spoke`` / ``frontend`` gateways get an ``mqtt-transmission`` attachment
277
+ plus the Transmission module (they publish Sparkplug to the broker);
278
+ - ``hub`` / ``backend`` gateways get an ``mqtt-engine`` attachment plus the
279
+ Engine module (they subscribe and aggregate);
280
+ - if NO gateway carries any of those roles (a basic shape), the single/first
281
+ gateway gets BOTH attachments + both modules - a self-contained demo loop
282
+ through the broker.
283
+
284
+ Brokers are not ``never_on_edge``, so an Edge spoke attaching with role
285
+ ``mqtt-transmission`` is correct and expected. Idempotent: guards on
286
+ ``(instance, role)`` attachment pairs and on module-already-present, so
287
+ ``apply_iiot(apply_iiot(c)) == apply_iiot(c)``. The expansion of a redundancy
288
+ master happens later in :func:`~ignition_stack.services.resolver.resolve`,
289
+ which copies the master's attachments + modules onto the backup, so a
290
+ redundant Engine gateway ends up with the Engine module on both nodes.
291
+
292
+ Raises ``ValueError`` (surfaced by the CLI as exit code 2) when the broker
293
+ slug is unknown, is not an ``mqtt-broker``, or carries no ``wires.mqtt`` block.
294
+ """
295
+ if broker is None:
296
+ return config
297
+
298
+ from ignition_stack.services.loader import load_all_services
299
+
300
+ catalog = load_all_services()
301
+ manifest = catalog.get(broker)
302
+ if manifest is None:
303
+ brokers = ", ".join(sorted(slug for slug, m in catalog.items() if m.kind == "mqtt-broker"))
304
+ raise ValueError(f"unknown iiot broker '{broker}'; known mqtt brokers: {brokers}")
305
+ if manifest.kind != "mqtt-broker":
306
+ raise ValueError(f"iiot broker '{broker}' is a '{manifest.kind}' service, not an mqtt-broker")
307
+ if manifest.wires is None or manifest.wires.mqtt is None:
308
+ raise ValueError(f"mqtt broker '{broker}' declares no wires.mqtt block, so the IIoT overlay cannot find its Transmission/Engine module slugs")
309
+ mqtt = manifest.wires.mqtt
310
+
311
+ # Add the broker instance once (id == slug); the resolver's singleton check
312
+ # catches an accidental duplicate broker elsewhere in the registry.
313
+ if not any(inst.id == broker for inst in config.service_instances):
314
+ config.service_instances.append(ServiceInstance(id=broker, service=broker))
315
+
316
+ transmission_gws = [gw for gw in config.gateways if gw.role in _TRANSMISSION_ROLES]
317
+ engine_gws = [gw for gw in config.gateways if gw.role in _ENGINE_ROLES]
318
+
319
+ if not transmission_gws and not engine_gws:
320
+ # No transmission/engine roles in this topology: run the whole loop on
321
+ # the single/first gateway so the demo is self-contained.
322
+ only = config.gateways[0]
323
+ _wire_iiot_gateway(only, broker, "mqtt-transmission", mqtt.transmission_module)
324
+ _wire_iiot_gateway(only, broker, "mqtt-engine", mqtt.engine_module)
325
+ else:
326
+ for gw in transmission_gws:
327
+ _wire_iiot_gateway(gw, broker, "mqtt-transmission", mqtt.transmission_module)
328
+ for gw in engine_gws:
329
+ _wire_iiot_gateway(gw, broker, "mqtt-engine", mqtt.engine_module)
330
+ return config
331
+
332
+
333
+ def _wire_iiot_gateway(gw: GatewayConfig, instance_id: str, role: str, module: str) -> None:
334
+ """Attach ``gw`` to the broker with ``role`` and add ``module`` to it.
335
+
336
+ Role-aware guard (not the phase-1 instance-only ``_attach_all_gateways``): a
337
+ self-loop gateway holds two attachments to the same broker instance, one per
338
+ role, so the guard must key on the ``(instance, role)`` pair. Module presence
339
+ is guarded separately so a second pass adds nothing.
340
+ """
341
+ if not any(att.instance == instance_id and att.role == role for att in gw.services):
342
+ gw.services.append(ServiceAttachment(instance=instance_id, role=role))
343
+ if module not in gw.modules:
344
+ gw.modules.append(module)
@@ -0,0 +1,45 @@
1
+ """Basic architecture: one full Ignition gateway + optional SQL DB.
2
+
3
+ This mirrors Ignition's documented Basic architecture - a single gateway
4
+ that does everything - surfaced as a named architecture so the wizard can
5
+ offer it alongside the multi-gateway architectures. The only knobs are the
6
+ database choice (defaults to Postgres) and the optional reverse-proxy
7
+ scaffold.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+
14
+ from ignition_stack.architectures.base import Architecture, ArchOptions, register
15
+ from ignition_stack.config import DatabaseConfig, GatewayConfig, ProjectConfig
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class BasicArchitecture:
20
+ slug: str = "basic"
21
+ summary: str = "One full Ignition 8.3 gateway + Postgres. The default starter stack."
22
+
23
+ def build(self, name: str, options: ArchOptions) -> ProjectConfig:
24
+ gateway = GatewayConfig()
25
+ if options.edge_role in {"gateway", "basic"}:
26
+ gateway = gateway.model_copy(update={"ignition_edition": "edge"})
27
+
28
+ return ProjectConfig(
29
+ name=name,
30
+ architecture=self.slug,
31
+ gateways=[gateway],
32
+ database=_database(options),
33
+ services=list(options.services),
34
+ reverse_proxy=options.reverse_proxy,
35
+ )
36
+
37
+
38
+ def _database(options: ArchOptions) -> DatabaseConfig | None:
39
+ if options.database_kind is None:
40
+ return None
41
+ return DatabaseConfig(kind=options.database_kind)
42
+
43
+
44
+ # Side-effect: registers this architecture when the module is imported.
45
+ architecture: Architecture = register(BasicArchitecture())