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