ignition-stack 0.2.0__tar.gz → 0.4.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.
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/PKG-INFO +1 -1
- ignition_stack-0.4.0/builtin_modules.yaml +122 -0
- ignition_stack-0.4.0/ignition_stack/__init__.py +1 -0
- ignition_stack-0.4.0/ignition_stack/catalog/builtins.py +171 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/cli.py +51 -1
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/completion.py +15 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/compose/engine.py +63 -36
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/compose/templates/services/ignition.yaml.j2 +9 -6
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/config/schema.py +76 -30
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/postsetup/generator.py +41 -21
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/profiles/base.py +34 -1
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/profiles/hub_and_spoke.py +3 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/profiles/scaleout.py +10 -6
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/services/resolver.py +1 -0
- ignition_stack-0.4.0/ignition_stack/templates/post-setup/gateway-network-link.md.j2 +23 -0
- ignition_stack-0.4.0/ignition_stack/update_check.py +129 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/wizard.py +43 -2
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/pyproject.toml +3 -2
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/profiles/hub-and-spoke/docker-compose.yaml +29 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/profiles/scaleout/docker-compose.yaml +13 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/profiles/scaleout-redundant/docker-compose.yaml +8 -0
- ignition_stack-0.4.0/tests/test_builtin_catalog_smoke.py +114 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_compose_engine.py +3 -4
- ignition_stack-0.4.0/tests/test_disable_builtins.py +241 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_init_standalone.py +1 -24
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_profiles.py +110 -38
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_service_catalog.py +16 -8
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_service_catalog_smoke.py +3 -3
- ignition_stack-0.4.0/tests/test_update_check.py +108 -0
- ignition_stack-0.2.0/ignition_stack/__init__.py +0 -1
- ignition_stack-0.2.0/ignition_stack/templates/post-setup/gateway-network-link.md.j2 +0 -18
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/.gitignore +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/LICENSE +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/README.md +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/catalog/__init__.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/catalog/download.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/catalog/loader.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/catalog/schema.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/catalog/verify.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/commands/__init__.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/commands/modules.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/compose/__init__.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/compose/templates/footer.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/compose/templates/header.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/compose/templates/services/bootstrap.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/compose/writer.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/config/__init__.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/config/io.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/lifecycle/__init__.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/lifecycle/cleanup.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/lifecycle/record.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/lifecycle/regenerate.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/postsetup/__init__.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/profiles/__init__.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/profiles/advisory.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/profiles/mcp_n8n.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/profiles/standalone.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/services/__init__.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/services/loader.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/services/manifest.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/__init__.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/post-setup/_default.md.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/post-setup/device-connection.md.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/post-setup/identity-provider.md.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/post-setup/kafka-connector.md.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/post-setup/mcp-module.md.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/post-setup/mqtt-engine-connection.md.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/post-setup/opc-ua-connection.md.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/post-setup/redundancy-pairing.md.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/post-setup/reverse-proxy.md.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/redundancy/redundancy.xml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/chariot/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/chariot/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/chariot/seed/service/USAGE.md +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/emqx/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/emqx/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/emqx/seed/service/USAGE.md +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/hivemq/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/hivemq/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/hivemq/seed/service/USAGE.md +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/kafka/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/kafka/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/kafka/seed/service/USAGE.md +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/keycloak/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/keycloak/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/keycloak/seed/service/import/ignition-realm.json +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/mariadb/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/mariadb/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/mariadb/seed/service/initdb/00-create-extra-databases.sh +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/modbus-sim/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/modbus-sim/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/modbus-sim/seed/service/USAGE.md +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/mongo/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/mongo/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/mongo/seed/service/initdb/01-demo-collection.js +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/mysql/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/mysql/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/mysql/seed/service/initdb/00-create-extra-databases.sh +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/n8n/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/n8n/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/n8n/seed/service/USAGE.md +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/opcua-sim/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/opcua-sim/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/opcua-sim/seed/service/USAGE.md +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/postgres/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/postgres/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/config.json +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/resource.json +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/secret-provider/internal-secret-provider/config.json +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/secret-provider/internal-secret-provider/resource.json +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/postgres/seed/service/initdb/00-create-extra-databases.sh +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/rabbitmq/compose.yaml.j2 +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/rabbitmq/manifest.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/services/rabbitmq/seed/service/enabled_plugins +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/standalone-postgres/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/standalone-postgres/scripts/docker-bootstrap.sh +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/standalone-postgres/services/ignition/config/resources/core/config-mode.json +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/standalone-postgres/services/ignition/config/resources/dev/config-mode.json +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/ignition_stack/templates/standalone-postgres/services/ignition/projects/.gitkeep +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/modules.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/__init__.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/conftest.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/combos/network-split/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/combos/smoke-stack/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/profiles/mcp-n8n/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/profiles/standalone/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/scaleout-skeleton/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/chariot/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/db-mariadb/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/db-mongo/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/db-mysql/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/db-postgres/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/emqx/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/hivemq/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/kafka/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/keycloak/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/modbus-sim/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/n8n/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/opcua-sim/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/services/rabbitmq/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/golden/standalone-postgres/docker-compose.yaml +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_completion.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_declarative_io.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_docs_cli_reference.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_lifecycle.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_modules_catalog.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_modules_cli.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_postsetup.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/tests/test_redundancy.py +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/verification/redundancy-spike/README.md +0 -0
- {ignition_stack-0.2.0 → ignition_stack-0.4.0}/verification/smoke/README.md +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Catalog of the built-in IA modules that ship inside the Ignition gateway
|
|
2
|
+
# image. Used to translate a gateway's `disable_builtins` slugs into the
|
|
3
|
+
# GATEWAY_MODULES_ENABLED whitelist the engine emits (enabled = all built-ins
|
|
4
|
+
# minus the disabled ones, plus any third-party module identifiers).
|
|
5
|
+
#
|
|
6
|
+
# Why this file exists:
|
|
7
|
+
# GATEWAY_MODULES_ENABLED is a strict WHITELIST, not a blocklist - anything
|
|
8
|
+
# not listed is quarantined at boot (verified live on 8.3.6). So "disable
|
|
9
|
+
# Vision" can only be expressed as "enable every built-in except Vision".
|
|
10
|
+
# That inversion needs the COMPLETE built-in set; an incomplete list would
|
|
11
|
+
# silently quarantine the modules we forgot to enumerate. This file is that
|
|
12
|
+
# complete set, and tests/test_builtin_catalog.py (marked `smoke`) re-derives
|
|
13
|
+
# it from the live image so a stale list fails CI loudly instead of dropping
|
|
14
|
+
# modules silently.
|
|
15
|
+
#
|
|
16
|
+
# Provenance (how to regenerate when bumping the Ignition image):
|
|
17
|
+
# 1. Boot the pinned image with no whitelist so every built-in loads:
|
|
18
|
+
# docker run --rm -e ACCEPT_IGNITION_EULA=Y -e GATEWAY_ADMIN_PASSWORD=x \
|
|
19
|
+
# inductiveautomation/ignition:<tag>
|
|
20
|
+
# 2. Read the loaded set from the gateway log lines:
|
|
21
|
+
# Starting up module '<identifier>' ... module-name=<name>
|
|
22
|
+
# 3. Update `ignition_version` and the `modules` list below to match.
|
|
23
|
+
# The smoke guard test automates this comparison.
|
|
24
|
+
#
|
|
25
|
+
# `identifier` is the fully-qualified module id used verbatim in
|
|
26
|
+
# GATEWAY_MODULES_ENABLED. `slug` is the friendly kebab name a user puts in
|
|
27
|
+
# `disable_builtins`. `name` is the gateway's display name (for wizard labels).
|
|
28
|
+
|
|
29
|
+
version: 1
|
|
30
|
+
|
|
31
|
+
# The exact Ignition image tag this built-in set was captured from. The guard
|
|
32
|
+
# test only asserts a match when the running image reports this version.
|
|
33
|
+
ignition_version: "8.3.6"
|
|
34
|
+
|
|
35
|
+
modules:
|
|
36
|
+
- slug: alarm-notification
|
|
37
|
+
identifier: com.inductiveautomation.alarm-notification
|
|
38
|
+
name: Alarm Notification
|
|
39
|
+
- slug: allen-bradley-driver
|
|
40
|
+
identifier: com.inductiveautomation.opcua.drivers.ablegacy
|
|
41
|
+
name: Allen-Bradley Driver
|
|
42
|
+
- slug: bacnet-driver
|
|
43
|
+
identifier: com.inductiveautomation.opcua.drivers.bacnet
|
|
44
|
+
name: BACnet Driver
|
|
45
|
+
- slug: enterprise-administration
|
|
46
|
+
identifier: com.inductiveautomation.eam
|
|
47
|
+
name: Enterprise Administration
|
|
48
|
+
- slug: event-streams
|
|
49
|
+
identifier: com.inductiveautomation.eventstream
|
|
50
|
+
name: Event Streams
|
|
51
|
+
- slug: historian-core
|
|
52
|
+
identifier: com.inductiveautomation.historian
|
|
53
|
+
name: Historian Core
|
|
54
|
+
- slug: kafka-connector
|
|
55
|
+
identifier: com.inductiveautomation.connectors.kafka
|
|
56
|
+
name: Kafka Connector
|
|
57
|
+
- slug: legacy-dnp3-driver
|
|
58
|
+
identifier: com.inductiveautomation.opcua.drivers.dnp3
|
|
59
|
+
name: Legacy DNP3 Driver
|
|
60
|
+
- slug: logix-driver
|
|
61
|
+
identifier: com.inductiveautomation.opcua.drivers.logix
|
|
62
|
+
name: Logix Driver
|
|
63
|
+
- slug: mariadb-jdbc-driver
|
|
64
|
+
identifier: com.inductiveautomation.jdbc.mariadb
|
|
65
|
+
name: MariaDB JDBC Driver
|
|
66
|
+
- slug: micro800-driver
|
|
67
|
+
identifier: com.inductiveautomation.opcua.drivers.micro800
|
|
68
|
+
name: Micro800 Driver
|
|
69
|
+
- slug: mitsubishi-driver
|
|
70
|
+
identifier: com.inductiveautomation.opcua.drivers.mitsubishi
|
|
71
|
+
name: Mitsubishi Driver
|
|
72
|
+
- slug: modbus-driver
|
|
73
|
+
identifier: com.inductiveautomation.opcua.drivers.modbus
|
|
74
|
+
name: Modbus Driver
|
|
75
|
+
- slug: mssql-jdbc-driver
|
|
76
|
+
identifier: com.inductiveautomation.jdbc.mssql
|
|
77
|
+
name: MSSQL JDBC Driver
|
|
78
|
+
- slug: omron-driver
|
|
79
|
+
identifier: com.inductiveautomation.opcua.drivers.omron
|
|
80
|
+
name: Omron Driver
|
|
81
|
+
- slug: opc-ua
|
|
82
|
+
identifier: com.inductiveautomation.opcua
|
|
83
|
+
name: OPC-UA
|
|
84
|
+
- slug: perspective
|
|
85
|
+
identifier: com.inductiveautomation.perspective
|
|
86
|
+
name: Perspective
|
|
87
|
+
- slug: postgresql-jdbc-driver
|
|
88
|
+
identifier: com.inductiveautomation.jdbc.postgresql
|
|
89
|
+
name: PostgreSQL JDBC Driver
|
|
90
|
+
- slug: reporting
|
|
91
|
+
identifier: com.inductiveautomation.reporting
|
|
92
|
+
name: Reporting
|
|
93
|
+
- slug: sfc
|
|
94
|
+
identifier: com.inductiveautomation.sfc
|
|
95
|
+
name: SFC
|
|
96
|
+
- slug: siemens-drivers
|
|
97
|
+
identifier: com.inductiveautomation.opcua.drivers.siemens
|
|
98
|
+
name: Siemens Drivers
|
|
99
|
+
- slug: siemens-enhanced-driver
|
|
100
|
+
identifier: com.inductiveautomation.opcua.drivers.siemens-symbolic
|
|
101
|
+
name: Siemens Enhanced Driver
|
|
102
|
+
- slug: sms-notification
|
|
103
|
+
identifier: com.inductiveautomation.sms-notification
|
|
104
|
+
name: SMS Notification
|
|
105
|
+
- slug: sql-bridge
|
|
106
|
+
identifier: com.inductiveautomation.sqlbridge
|
|
107
|
+
name: SQL Bridge
|
|
108
|
+
- slug: sql-historian
|
|
109
|
+
identifier: com.inductiveautomation.historian.sql
|
|
110
|
+
name: SQL Historian
|
|
111
|
+
- slug: symbol-factory
|
|
112
|
+
identifier: com.inductiveautomation.symbol-factory
|
|
113
|
+
name: Symbol Factory
|
|
114
|
+
- slug: udp-and-tcp-drivers
|
|
115
|
+
identifier: com.inductiveautomation.opcua.drivers.tcpudp
|
|
116
|
+
name: UDP and TCP Drivers
|
|
117
|
+
- slug: vision
|
|
118
|
+
identifier: com.inductiveautomation.vision
|
|
119
|
+
name: Vision
|
|
120
|
+
- slug: webdev
|
|
121
|
+
identifier: com.inductiveautomation.webdev
|
|
122
|
+
name: WebDev
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Load and query the built-in IA module catalog (``builtin_modules.yaml``).
|
|
2
|
+
|
|
3
|
+
The third-party catalog (``modules.yaml`` + ``catalog/schema.py``) covers
|
|
4
|
+
modules the CLI *adds*. This module covers the modules that *already ship*
|
|
5
|
+
inside the gateway image, which the engine needs in order to translate a
|
|
6
|
+
gateway's ``disable_builtins`` slugs into a ``GATEWAY_MODULES_ENABLED``
|
|
7
|
+
whitelist.
|
|
8
|
+
|
|
9
|
+
The whitelist is strict: anything not listed is quarantined at boot. So
|
|
10
|
+
"disable Vision" is expressed as "enable every built-in except Vision",
|
|
11
|
+
which requires the *complete* built-in set - hence a pinned data file plus a
|
|
12
|
+
smoke guard test that re-derives it from the live image.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from importlib import resources
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Annotated
|
|
21
|
+
|
|
22
|
+
import yaml
|
|
23
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BuiltinCatalogLoadError(Exception):
|
|
27
|
+
"""Raised when builtin_modules.yaml cannot be read or fails validation."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DEFAULT_BUILTIN_CATALOG_NAME = "builtin_modules.yaml"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BuiltinModule(BaseModel):
|
|
34
|
+
"""One built-in IA module that ships inside the gateway image."""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
37
|
+
|
|
38
|
+
slug: Annotated[
|
|
39
|
+
str,
|
|
40
|
+
Field(
|
|
41
|
+
min_length=1,
|
|
42
|
+
pattern=r"^[a-z0-9][a-z0-9-]*$",
|
|
43
|
+
description="Friendly kebab name a user puts in `disable_builtins`.",
|
|
44
|
+
),
|
|
45
|
+
]
|
|
46
|
+
identifier: Annotated[
|
|
47
|
+
str,
|
|
48
|
+
Field(
|
|
49
|
+
min_length=1,
|
|
50
|
+
pattern=r"^[a-z0-9.-]+$",
|
|
51
|
+
description="Fully-qualified module id, used verbatim in GATEWAY_MODULES_ENABLED.",
|
|
52
|
+
),
|
|
53
|
+
]
|
|
54
|
+
name: Annotated[str, Field(min_length=1, description="Gateway display name (wizard label).")]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BuiltinCatalog(BaseModel):
|
|
58
|
+
"""Top-level shape of builtin_modules.yaml."""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
61
|
+
|
|
62
|
+
version: Annotated[int, Field(ge=1)]
|
|
63
|
+
ignition_version: Annotated[
|
|
64
|
+
str,
|
|
65
|
+
Field(min_length=1, description="Image tag this built-in set was captured from."),
|
|
66
|
+
]
|
|
67
|
+
modules: Annotated[list[BuiltinModule], Field(min_length=1)]
|
|
68
|
+
|
|
69
|
+
@field_validator("modules")
|
|
70
|
+
@classmethod
|
|
71
|
+
def _slugs_unique(cls, modules: list[BuiltinModule]) -> list[BuiltinModule]:
|
|
72
|
+
slugs = [m.slug for m in modules]
|
|
73
|
+
dupes = sorted({s for s in slugs if slugs.count(s) > 1})
|
|
74
|
+
if dupes:
|
|
75
|
+
raise ValueError(f"duplicate built-in slugs: {', '.join(dupes)}")
|
|
76
|
+
return modules
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def slugs(self) -> set[str]:
|
|
80
|
+
"""Every known built-in slug."""
|
|
81
|
+
return {m.slug for m in self.modules}
|
|
82
|
+
|
|
83
|
+
def identifiers_excluding(self, disabled_slugs: list[str]) -> list[str]:
|
|
84
|
+
"""FQ identifiers of every built-in whose slug is not in ``disabled_slugs``.
|
|
85
|
+
|
|
86
|
+
Order follows the catalog (already alphabetical by slug) so generated
|
|
87
|
+
whitelists are deterministic and golden-stable.
|
|
88
|
+
"""
|
|
89
|
+
disabled = set(disabled_slugs)
|
|
90
|
+
return [m.identifier for m in self.modules if m.slug not in disabled]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_builtin_catalog(path: Path | None = None) -> BuiltinCatalog:
|
|
94
|
+
"""Load and validate the built-in catalog.
|
|
95
|
+
|
|
96
|
+
With ``path=None`` the catalog shipped with the installed package is used;
|
|
97
|
+
a path overrides it (test fixtures). Mirrors ``catalog.loader.load_catalog``.
|
|
98
|
+
"""
|
|
99
|
+
yaml_text = _read_yaml_text(path)
|
|
100
|
+
name = DEFAULT_BUILTIN_CATALOG_NAME
|
|
101
|
+
try:
|
|
102
|
+
raw = yaml.safe_load(yaml_text)
|
|
103
|
+
except yaml.YAMLError as exc:
|
|
104
|
+
raise BuiltinCatalogLoadError(f"{name} is not valid YAML: {exc}") from exc
|
|
105
|
+
|
|
106
|
+
if not isinstance(raw, dict):
|
|
107
|
+
raise BuiltinCatalogLoadError(f"{name} top-level must be a mapping.")
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
return BuiltinCatalog.model_validate(raw)
|
|
111
|
+
except ValidationError as exc:
|
|
112
|
+
raise BuiltinCatalogLoadError(
|
|
113
|
+
f"{DEFAULT_BUILTIN_CATALOG_NAME} failed schema validation:\n{exc}"
|
|
114
|
+
) from exc
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@lru_cache(maxsize=1)
|
|
118
|
+
def default_builtin_catalog() -> BuiltinCatalog:
|
|
119
|
+
"""The built-in catalog shipped with the package, loaded once and cached.
|
|
120
|
+
|
|
121
|
+
The data file is immutable package data, so both config validation and the
|
|
122
|
+
compose engine can share a single memoized read rather than re-parsing YAML
|
|
123
|
+
on every gateway.
|
|
124
|
+
"""
|
|
125
|
+
return load_builtin_catalog()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def builtin_slugs() -> frozenset[str]:
|
|
129
|
+
"""Slugs of the shipped built-in catalog, for cheap ``disable_builtins`` validation."""
|
|
130
|
+
return frozenset(default_builtin_catalog().slugs)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_disable_slugs(slugs: list[str]) -> None:
|
|
134
|
+
"""Raise ``ValueError`` if any slug is not a known built-in.
|
|
135
|
+
|
|
136
|
+
Shared by ``GatewayConfig`` field validation (construction-time) and
|
|
137
|
+
``profiles.apply_disable_builtins`` (post-construction mutation, which
|
|
138
|
+
pydantic does not re-validate), so the wizard/CLI path is guarded too. A
|
|
139
|
+
typo would otherwise be a silent no-op - the slug just isn't disabled.
|
|
140
|
+
"""
|
|
141
|
+
known = builtin_slugs()
|
|
142
|
+
unknown = [s for s in slugs if s not in known]
|
|
143
|
+
if unknown:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"unknown built-in module slug(s): {', '.join(unknown)}. "
|
|
146
|
+
f"Valid slugs are: {', '.join(sorted(known))}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _read_yaml_text(path: Path | None) -> str:
|
|
151
|
+
if path is not None:
|
|
152
|
+
if not path.is_file():
|
|
153
|
+
raise BuiltinCatalogLoadError(f"Built-in catalog not found at {path}.")
|
|
154
|
+
return path.read_text(encoding="utf-8")
|
|
155
|
+
|
|
156
|
+
# Installed wheels: force-included as ignition_stack/builtin_modules.yaml.
|
|
157
|
+
# Editable dev installs: it lives at the repo root next to pyproject.toml.
|
|
158
|
+
try:
|
|
159
|
+
bundled = resources.files("ignition_stack").joinpath(DEFAULT_BUILTIN_CATALOG_NAME)
|
|
160
|
+
if bundled.is_file():
|
|
161
|
+
return bundled.read_text(encoding="utf-8")
|
|
162
|
+
except (FileNotFoundError, OSError, ModuleNotFoundError):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
166
|
+
dev_path = repo_root / DEFAULT_BUILTIN_CATALOG_NAME
|
|
167
|
+
if not dev_path.is_file():
|
|
168
|
+
raise BuiltinCatalogLoadError(
|
|
169
|
+
f"{DEFAULT_BUILTIN_CATALOG_NAME} not found in package data or at {dev_path}."
|
|
170
|
+
)
|
|
171
|
+
return dev_path.read_text(encoding="utf-8")
|
|
@@ -22,6 +22,7 @@ from rich.console import Console
|
|
|
22
22
|
from ignition_stack import __version__
|
|
23
23
|
from ignition_stack.commands.modules import modules_app
|
|
24
24
|
from ignition_stack.completion import (
|
|
25
|
+
complete_disable_builtin,
|
|
25
26
|
complete_edge_role,
|
|
26
27
|
complete_output_format,
|
|
27
28
|
complete_profile,
|
|
@@ -55,6 +56,11 @@ from ignition_stack.profiles import (
|
|
|
55
56
|
list_profiles,
|
|
56
57
|
)
|
|
57
58
|
from ignition_stack.services.resolver import resolve
|
|
59
|
+
from ignition_stack.update_check import (
|
|
60
|
+
check_for_update,
|
|
61
|
+
detect_upgrade_command,
|
|
62
|
+
should_notify,
|
|
63
|
+
)
|
|
58
64
|
from ignition_stack.wizard import run_wizard
|
|
59
65
|
|
|
60
66
|
app = typer.Typer(
|
|
@@ -88,6 +94,27 @@ def _root(
|
|
|
88
94
|
if ctx.invoked_subcommand is None:
|
|
89
95
|
console.print(ctx.get_help())
|
|
90
96
|
raise typer.Exit()
|
|
97
|
+
_notify_update_available()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _notify_update_available() -> None:
|
|
101
|
+
"""Print a one-line advisory when a newer release is on PyPI.
|
|
102
|
+
|
|
103
|
+
Runs only for real subcommands (not --version or bare help) and only on an
|
|
104
|
+
interactive terminal. Best-effort: any failure inside the check is swallowed
|
|
105
|
+
rather than allowed to disrupt the command the user actually ran.
|
|
106
|
+
"""
|
|
107
|
+
if not should_notify():
|
|
108
|
+
return
|
|
109
|
+
result = check_for_update()
|
|
110
|
+
if result is None:
|
|
111
|
+
return
|
|
112
|
+
current, latest = result
|
|
113
|
+
console.print(
|
|
114
|
+
f"[dim]update available[/dim] [yellow]{current}[/yellow] -> "
|
|
115
|
+
f"[green]{latest}[/green] · run: [cyan]{detect_upgrade_command()}[/cyan]",
|
|
116
|
+
highlight=False,
|
|
117
|
+
)
|
|
91
118
|
|
|
92
119
|
|
|
93
120
|
def _profile_help() -> str:
|
|
@@ -172,6 +199,17 @@ def init(
|
|
|
172
199
|
),
|
|
173
200
|
autocompletion=complete_redundant_role,
|
|
174
201
|
),
|
|
202
|
+
disable_builtin: list[str] = typer.Option( # noqa: B008 - Typer pattern
|
|
203
|
+
[],
|
|
204
|
+
"--disable-builtin",
|
|
205
|
+
help=(
|
|
206
|
+
"Built-in IA module to turn off on every gateway (repeatable), e.g. "
|
|
207
|
+
"--disable-builtin vision --disable-builtin sfc. Emits a "
|
|
208
|
+
"GATEWAY_MODULES_ENABLED whitelist of everything else. Slugs "
|
|
209
|
+
"tab-complete; an unknown slug is rejected with the full valid list."
|
|
210
|
+
),
|
|
211
|
+
autocompletion=complete_disable_builtin,
|
|
212
|
+
),
|
|
175
213
|
from_file: Path | None = typer.Option( # noqa: B008 - Typer pattern
|
|
176
214
|
None,
|
|
177
215
|
"--from-file",
|
|
@@ -240,6 +278,7 @@ def init(
|
|
|
240
278
|
reverse_proxy=reverse_proxy,
|
|
241
279
|
proxy_path=proxy_path,
|
|
242
280
|
redundant=redundant,
|
|
281
|
+
disable_builtin=disable_builtin,
|
|
243
282
|
)
|
|
244
283
|
|
|
245
284
|
if dry_run:
|
|
@@ -328,6 +367,7 @@ def _build_from_profile(
|
|
|
328
367
|
reverse_proxy: str | None,
|
|
329
368
|
proxy_path: str,
|
|
330
369
|
redundant: str | None,
|
|
370
|
+
disable_builtin: list[str],
|
|
331
371
|
) -> ProjectConfig:
|
|
332
372
|
"""Materialize a config from the named profile + CLI flags, or exit cleanly."""
|
|
333
373
|
try:
|
|
@@ -345,6 +385,7 @@ def _build_from_profile(
|
|
|
345
385
|
network_split=network_split,
|
|
346
386
|
reverse_proxy=proxy,
|
|
347
387
|
redundant_role=redundant,
|
|
388
|
+
disable_builtins=tuple(disable_builtin),
|
|
348
389
|
)
|
|
349
390
|
try:
|
|
350
391
|
config = build_profile(profile, name, options)
|
|
@@ -463,7 +504,9 @@ def _options_from_config(config: ProjectConfig) -> ProfileOptions:
|
|
|
463
504
|
'none' to keep the new profile from re-introducing its edge default); the
|
|
464
505
|
spoke count from the number of spoke-role gateways, the frontend count from
|
|
465
506
|
the number of frontend-role gateways, and the network split is carried over
|
|
466
|
-
verbatim so a reshape preserves the user's topology choice.
|
|
507
|
+
verbatim so a reshape preserves the user's topology choice. Disabled
|
|
508
|
+
built-in modules are carried over too (see below) so a reshape doesn't
|
|
509
|
+
silently re-enable Vision/SFC/etc.
|
|
467
510
|
"""
|
|
468
511
|
edge_roles = [gw.role or gw.name for gw in config.gateways if gw.ignition_edition == "edge"]
|
|
469
512
|
spoke_count = sum(1 for gw in config.gateways if (gw.role or "") == "spoke")
|
|
@@ -479,6 +522,12 @@ def _options_from_config(config: ProjectConfig) -> ProfileOptions:
|
|
|
479
522
|
),
|
|
480
523
|
None,
|
|
481
524
|
)
|
|
525
|
+
# Disabled built-ins are applied stack-wide, so carry over the slugs disabled
|
|
526
|
+
# on EVERY gateway (the intersection) - that is the stack-wide intent, and it
|
|
527
|
+
# won't over-disable a module that a hand-authored config turned off on only
|
|
528
|
+
# one node. The target profile re-applies it uniformly.
|
|
529
|
+
disabled_sets = [set(gw.disable_builtins) for gw in config.gateways]
|
|
530
|
+
disable_builtins = tuple(sorted(set.intersection(*disabled_sets))) if disabled_sets else ()
|
|
482
531
|
return ProfileOptions(
|
|
483
532
|
spokes=spoke_count or 3,
|
|
484
533
|
frontends=frontend_count or 1,
|
|
@@ -488,6 +537,7 @@ def _options_from_config(config: ProjectConfig) -> ProfileOptions:
|
|
|
488
537
|
database_kind=config.database.kind if config.database else None,
|
|
489
538
|
services=tuple(config.services),
|
|
490
539
|
redundant_role=redundant_role,
|
|
540
|
+
disable_builtins=disable_builtins,
|
|
491
541
|
)
|
|
492
542
|
|
|
493
543
|
|
|
@@ -77,3 +77,18 @@ def complete_module_name(incomplete: str) -> list[str]:
|
|
|
77
77
|
# catalog must degrade to "no suggestions", never break the shell line.
|
|
78
78
|
return []
|
|
79
79
|
return [entry.name for entry in entries if entry.name.startswith(incomplete)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def complete_disable_builtin(incomplete: str) -> list[tuple[str, str]]:
|
|
83
|
+
"""Built-in module slugs (with display name) matching the typed prefix.
|
|
84
|
+
|
|
85
|
+
Reads the bundled built-in catalog; degrades to no suggestions on any error
|
|
86
|
+
so a TAB never breaks the shell line.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
from ignition_stack.catalog.builtins import default_builtin_catalog
|
|
90
|
+
|
|
91
|
+
modules = default_builtin_catalog().modules
|
|
92
|
+
except Exception:
|
|
93
|
+
return []
|
|
94
|
+
return [(m.slug, m.name) for m in modules if m.slug.startswith(incomplete)]
|
|
@@ -39,6 +39,7 @@ from typing import TYPE_CHECKING
|
|
|
39
39
|
from jinja2 import Environment, PackageLoader, StrictUndefined
|
|
40
40
|
from ruamel.yaml import YAML
|
|
41
41
|
|
|
42
|
+
from ignition_stack.catalog.builtins import default_builtin_catalog
|
|
42
43
|
from ignition_stack.services.loader import load_all_services, load_service
|
|
43
44
|
|
|
44
45
|
if TYPE_CHECKING:
|
|
@@ -131,10 +132,7 @@ def _render_database(config: ProjectConfig) -> str:
|
|
|
131
132
|
"""
|
|
132
133
|
db = config.database
|
|
133
134
|
assert db is not None
|
|
134
|
-
if config.is_multi_gateway
|
|
135
|
-
container_name_ref = f"{db.name}-${{COMPOSE_PROJECT_NAME}}"
|
|
136
|
-
else:
|
|
137
|
-
container_name_ref = f"{db.name}-${{GATEWAY_NAME}}"
|
|
135
|
+
container_name_ref = f"{db.name}-${{COMPOSE_PROJECT_NAME}}" if config.is_multi_gateway else f"{db.name}-${{GATEWAY_NAME}}"
|
|
138
136
|
tpl = _service_jinja_env().get_template(f"{db.kind}/compose.yaml.j2")
|
|
139
137
|
return tpl.render(
|
|
140
138
|
name=db.name,
|
|
@@ -186,9 +184,7 @@ def _service_dependencies(manifest: object, config: ProjectConfig) -> list[str]:
|
|
|
186
184
|
return deps
|
|
187
185
|
|
|
188
186
|
|
|
189
|
-
def _gateway_context(
|
|
190
|
-
gw: GatewayConfig, config: ProjectConfig, catalog: Catalog | None
|
|
191
|
-
) -> dict[str, object]:
|
|
187
|
+
def _gateway_context(gw: GatewayConfig, config: ProjectConfig, catalog: Catalog | None) -> dict[str, object]:
|
|
192
188
|
"""Build the per-gateway context dict shared by the bootstrap + gateway fragments."""
|
|
193
189
|
multi = config.is_multi_gateway
|
|
194
190
|
|
|
@@ -215,10 +211,7 @@ def _gateway_context(
|
|
|
215
211
|
# (DB/broker access). Gateways with no role tag default to
|
|
216
212
|
# frontend membership; explicit role=backend lands a gateway on
|
|
217
213
|
# only the backend (rare; used for backend-only edge cases).
|
|
218
|
-
if gw.role == "backend"
|
|
219
|
-
networks = [NETWORK_BACKEND]
|
|
220
|
-
else:
|
|
221
|
-
networks = [NETWORK_FRONTEND, NETWORK_BACKEND]
|
|
214
|
+
networks = [NETWORK_BACKEND] if gw.role == "backend" else [NETWORK_FRONTEND, NETWORK_BACKEND]
|
|
222
215
|
|
|
223
216
|
module_identifiers = _module_identifiers_for(gw, catalog)
|
|
224
217
|
cached_modules = bool(gw.modules)
|
|
@@ -253,24 +246,40 @@ def _bootstrap_context(ctx: dict[str, object]) -> dict[str, object]:
|
|
|
253
246
|
}
|
|
254
247
|
|
|
255
248
|
|
|
256
|
-
def _ignition_context(
|
|
257
|
-
ctx: dict[str, object], config: ProjectConfig, multi: bool
|
|
258
|
-
) -> dict[str, object]:
|
|
249
|
+
def _ignition_context(ctx: dict[str, object], config: ProjectConfig, multi: bool) -> dict[str, object]:
|
|
259
250
|
gw: GatewayConfig = ctx["gw"] # type: ignore[assignment]
|
|
260
251
|
# IGNITION_EDITION lives in the anchor as "standard", so only emit an
|
|
261
252
|
# override when this gateway differs - keeps Phase 2's environment
|
|
262
253
|
# block as the bare anchor reference.
|
|
263
254
|
edition_override = gw.ignition_edition if gw.ignition_edition != "standard" else None
|
|
264
255
|
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
256
|
+
# Gateway Network wiring (Phase 4, per the verified Phase-3 spike, which the
|
|
257
|
+
# spike itself notes mirrors the publicdemo-all dev stack: 8088 / no-SSL /
|
|
258
|
+
# Unrestricted). Two kinds of GAN link ride this same plain, auto-approving
|
|
259
|
+
# path:
|
|
260
|
+
# - Redundancy: the backup points an outgoing connection at its master and
|
|
261
|
+
# must NOT be renamed via -n (it adopts the master's system name on sync).
|
|
262
|
+
# - Multi-gateway profiles: each gateway names its peers in gan_outgoing
|
|
263
|
+
# (scaleout frontend -> backend, hub-and-spoke spoke -> hub).
|
|
264
|
+
# Every GAN participant carries the full open incoming block. Mirroring the
|
|
265
|
+
# spike's BOTH-ends shape keeps requireSSL=false on the *initiator* too -
|
|
266
|
+
# the spike flagged that as the load-bearing setting for a plain link, so we
|
|
267
|
+
# don't shrink it to receiver-only.
|
|
272
268
|
is_redundant = gw.redundancy is not None
|
|
273
269
|
is_backup = is_redundant and gw.redundancy.mode == "backup"
|
|
270
|
+
|
|
271
|
+
# HOST/PORT/ENABLESSL trio per outgoing connection (all plain, SSL off).
|
|
272
|
+
gan_outgoing: list[dict[str, object]] = []
|
|
273
|
+
if is_backup:
|
|
274
|
+
gan_outgoing.append({"host": gw.redundancy.peer, "port": gw.redundancy.gan_port})
|
|
275
|
+
gan_outgoing.extend({"host": peer, "port": 8088} for peer in gw.gan_outgoing)
|
|
276
|
+
|
|
277
|
+
# A gateway opens the Unrestricted incoming policy when it takes part in the
|
|
278
|
+
# GAN at all: it is a redundancy node, it initiates a connection, or some
|
|
279
|
+
# other gateway opens one to it (hub/backend as a link target).
|
|
280
|
+
gan_targets = {peer for other in config.gateways for peer in other.gan_outgoing}
|
|
281
|
+
gan_incoming = is_redundant or bool(gan_outgoing) or gw.name in gan_targets
|
|
282
|
+
|
|
274
283
|
return {
|
|
275
284
|
"service_name": ctx["service_name"],
|
|
276
285
|
"bootstrap_service_name": ctx["bootstrap_service_name"],
|
|
@@ -280,12 +289,16 @@ def _ignition_context(
|
|
|
280
289
|
"memory_mb": gw.memory_mb,
|
|
281
290
|
"edition_override": edition_override,
|
|
282
291
|
"module_identifiers": ctx["module_identifiers"],
|
|
292
|
+
# disable_active drives template emission (not the value's truthiness) so
|
|
293
|
+
# that disabling EVERY built-in emits an empty whitelist - which quarantines
|
|
294
|
+
# all, matching intent - instead of omitting the var and re-enabling all.
|
|
295
|
+
"disable_active": bool(gw.disable_builtins),
|
|
296
|
+
"modules_enabled": _modules_enabled_for(gw, ctx["module_identifiers"]), # type: ignore[arg-type]
|
|
283
297
|
"database_service": config.database.name if config.database else None,
|
|
284
298
|
"networks": ctx["networks"],
|
|
285
|
-
"redundant": is_redundant,
|
|
286
299
|
"rename": not is_backup,
|
|
287
|
-
"
|
|
288
|
-
"
|
|
300
|
+
"gan_incoming": gan_incoming,
|
|
301
|
+
"gan_outgoing": gan_outgoing,
|
|
289
302
|
}
|
|
290
303
|
|
|
291
304
|
|
|
@@ -294,19 +307,13 @@ def _module_identifiers_for(gw: GatewayConfig, catalog: Catalog | None) -> str:
|
|
|
294
307
|
if not gw.modules:
|
|
295
308
|
return ""
|
|
296
309
|
if catalog is None:
|
|
297
|
-
raise ValueError(
|
|
298
|
-
f"gateway '{gw.name}' lists modules {gw.modules} but no catalog "
|
|
299
|
-
"was passed to render_compose; load modules.yaml first"
|
|
300
|
-
)
|
|
310
|
+
raise ValueError(f"gateway '{gw.name}' lists modules {gw.modules} but no catalog " "was passed to render_compose; load modules.yaml first")
|
|
301
311
|
identifiers: list[str] = []
|
|
302
312
|
for slug in gw.modules:
|
|
303
313
|
try:
|
|
304
314
|
entry = catalog.by_name(slug)
|
|
305
315
|
except KeyError as exc:
|
|
306
|
-
raise ValueError(
|
|
307
|
-
f"gateway '{gw.name}' references unknown module '{slug}'; "
|
|
308
|
-
"check modules.yaml and the gateway config"
|
|
309
|
-
) from exc
|
|
316
|
+
raise ValueError(f"gateway '{gw.name}' references unknown module '{slug}'; " "check modules.yaml and the gateway config") from exc
|
|
310
317
|
# Modules-only env vars: JDBC drivers shouldn't be enumerated here.
|
|
311
318
|
if not _is_module(entry):
|
|
312
319
|
continue
|
|
@@ -314,6 +321,29 @@ def _module_identifiers_for(gw: GatewayConfig, catalog: Catalog | None) -> str:
|
|
|
314
321
|
return ",".join(identifiers)
|
|
315
322
|
|
|
316
323
|
|
|
324
|
+
def _modules_enabled_for(gw: GatewayConfig, module_identifiers: str) -> str:
|
|
325
|
+
"""The GATEWAY_MODULES_ENABLED whitelist VALUE for this gateway.
|
|
326
|
+
|
|
327
|
+
GATEWAY_MODULES_ENABLED is a strict whitelist: if set, every built-in not
|
|
328
|
+
listed is quarantined at boot. The template emits the var based on
|
|
329
|
+
``disable_active`` (whether any built-in was disabled), not on this value's
|
|
330
|
+
truthiness - so disabling every built-in yields an empty string here and an
|
|
331
|
+
empty whitelist downstream (quarantines all), rather than silently omitting
|
|
332
|
+
the var and re-enabling everything.
|
|
333
|
+
|
|
334
|
+
When something is disabled the whitelist must be complete: every built-in the
|
|
335
|
+
user did not disable, PLUS any third-party modules we added
|
|
336
|
+
(``module_identifiers``) - or those added modules would be quarantined too.
|
|
337
|
+
Returns '' when nothing is disabled (the var is omitted in that case).
|
|
338
|
+
"""
|
|
339
|
+
if not gw.disable_builtins:
|
|
340
|
+
return ""
|
|
341
|
+
enabled = default_builtin_catalog().identifiers_excluding(gw.disable_builtins)
|
|
342
|
+
added = [ident for ident in module_identifiers.split(",") if ident]
|
|
343
|
+
enabled.extend(ident for ident in added if ident not in enabled)
|
|
344
|
+
return ",".join(enabled)
|
|
345
|
+
|
|
346
|
+
|
|
317
347
|
def _is_module(entry: ModuleEntry | object) -> bool:
|
|
318
348
|
return getattr(entry, "kind", None) == "module"
|
|
319
349
|
|
|
@@ -347,10 +377,7 @@ def _describe(config: ProjectConfig) -> str:
|
|
|
347
377
|
"""Human-readable header comment summarizing the stack."""
|
|
348
378
|
n = len(config.gateways)
|
|
349
379
|
if n == 1 and config.database and config.database.kind == "postgres" and not config.services:
|
|
350
|
-
return
|
|
351
|
-
"Walking skeleton: one Ignition 8.3 gateway, one Postgres, "
|
|
352
|
-
"env-driven commissioning so first boot needs no UI."
|
|
353
|
-
)
|
|
380
|
+
return "Walking skeleton: one Ignition 8.3 gateway, one Postgres, " "env-driven commissioning so first boot needs no UI."
|
|
354
381
|
parts = [f"{n} Ignition 8.3 gateway{'s' if n != 1 else ''}"]
|
|
355
382
|
if config.database:
|
|
356
383
|
parts.append(f"one {config.database.kind}")
|
|
@@ -22,18 +22,21 @@
|
|
|
22
22
|
ACCEPT_MODULE_LICENSES: "{{ module_identifiers }}"
|
|
23
23
|
ACCEPT_MODULE_CERTS: "{{ module_identifiers }}"
|
|
24
24
|
{%- endif %}
|
|
25
|
-
{%- if
|
|
25
|
+
{%- if disable_active %}
|
|
26
|
+
GATEWAY_MODULES_ENABLED: "{{ modules_enabled }}"
|
|
27
|
+
{%- endif %}
|
|
28
|
+
{%- if gan_incoming %}
|
|
26
29
|
GATEWAY_NETWORK_ENABLED: "true"
|
|
27
30
|
GATEWAY_NETWORK_ALLOWINCOMING: "true"
|
|
28
31
|
GATEWAY_NETWORK_SECURITYPOLICY: "Unrestricted"
|
|
29
32
|
GATEWAY_NETWORK_REQUIRESSL: "false"
|
|
30
33
|
GATEWAY_NETWORK_REQUIRETWOWAYAUTH: "false"
|
|
31
34
|
{%- endif %}
|
|
32
|
-
{%-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{%-
|
|
35
|
+
{%- for conn in gan_outgoing %}
|
|
36
|
+
GATEWAY_NETWORK_{{ loop.index0 }}_HOST: "{{ conn.host }}"
|
|
37
|
+
GATEWAY_NETWORK_{{ loop.index0 }}_PORT: "{{ conn.port }}"
|
|
38
|
+
GATEWAY_NETWORK_{{ loop.index0 }}_ENABLESSL: "false"
|
|
39
|
+
{%- endfor %}
|
|
37
40
|
command: >
|
|
38
41
|
{%- if rename %}
|
|
39
42
|
-n {{ gateway_name_ref }}
|