ignition-stack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. ignition_stack/__init__.py +1 -0
  2. ignition_stack/catalog/__init__.py +10 -0
  3. ignition_stack/catalog/download.py +145 -0
  4. ignition_stack/catalog/loader.py +65 -0
  5. ignition_stack/catalog/schema.py +158 -0
  6. ignition_stack/catalog/verify.py +72 -0
  7. ignition_stack/cli.py +354 -0
  8. ignition_stack/commands/__init__.py +0 -0
  9. ignition_stack/commands/modules.py +178 -0
  10. ignition_stack/completion.py +46 -0
  11. ignition_stack/compose/__init__.py +4 -0
  12. ignition_stack/compose/engine.py +397 -0
  13. ignition_stack/compose/templates/footer.yaml.j2 +12 -0
  14. ignition_stack/compose/templates/header.yaml.j2 +14 -0
  15. ignition_stack/compose/templates/services/bootstrap.yaml.j2 +19 -0
  16. ignition_stack/compose/templates/services/ignition.yaml.j2 +35 -0
  17. ignition_stack/compose/writer.py +428 -0
  18. ignition_stack/config/__init__.py +8 -0
  19. ignition_stack/config/schema.py +311 -0
  20. ignition_stack/lifecycle/__init__.py +31 -0
  21. ignition_stack/lifecycle/cleanup.py +71 -0
  22. ignition_stack/lifecycle/record.py +67 -0
  23. ignition_stack/lifecycle/regenerate.py +62 -0
  24. ignition_stack/modules.yaml +83 -0
  25. ignition_stack/postsetup/__init__.py +3 -0
  26. ignition_stack/postsetup/generator.py +187 -0
  27. ignition_stack/profiles/__init__.py +27 -0
  28. ignition_stack/profiles/advisory.py +132 -0
  29. ignition_stack/profiles/base.py +108 -0
  30. ignition_stack/profiles/hub_and_spoke.py +87 -0
  31. ignition_stack/profiles/mcp_n8n.py +55 -0
  32. ignition_stack/profiles/scaleout.py +65 -0
  33. ignition_stack/profiles/standalone.py +44 -0
  34. ignition_stack/services/__init__.py +25 -0
  35. ignition_stack/services/loader.py +69 -0
  36. ignition_stack/services/manifest.py +106 -0
  37. ignition_stack/services/resolver.py +133 -0
  38. ignition_stack/templates/__init__.py +0 -0
  39. ignition_stack/templates/post-setup/_default.md.j2 +12 -0
  40. ignition_stack/templates/post-setup/device-connection.md.j2 +11 -0
  41. ignition_stack/templates/post-setup/gateway-network-link.md.j2 +18 -0
  42. ignition_stack/templates/post-setup/identity-provider.md.j2 +13 -0
  43. ignition_stack/templates/post-setup/kafka-connector.md.j2 +11 -0
  44. ignition_stack/templates/post-setup/mcp-module.md.j2 +11 -0
  45. ignition_stack/templates/post-setup/mqtt-engine-connection.md.j2 +11 -0
  46. ignition_stack/templates/post-setup/opc-ua-connection.md.j2 +11 -0
  47. ignition_stack/templates/post-setup/reverse-proxy.md.j2 +8 -0
  48. ignition_stack/templates/services/chariot/compose.yaml.j2 +17 -0
  49. ignition_stack/templates/services/chariot/manifest.yaml +22 -0
  50. ignition_stack/templates/services/chariot/seed/service/USAGE.md +14 -0
  51. ignition_stack/templates/services/emqx/compose.yaml.j2 +16 -0
  52. ignition_stack/templates/services/emqx/manifest.yaml +21 -0
  53. ignition_stack/templates/services/emqx/seed/service/USAGE.md +11 -0
  54. ignition_stack/templates/services/hivemq/compose.yaml.j2 +12 -0
  55. ignition_stack/templates/services/hivemq/manifest.yaml +19 -0
  56. ignition_stack/templates/services/hivemq/seed/service/USAGE.md +16 -0
  57. ignition_stack/templates/services/kafka/compose.yaml.j2 +27 -0
  58. ignition_stack/templates/services/kafka/manifest.yaml +20 -0
  59. ignition_stack/templates/services/kafka/seed/service/USAGE.md +17 -0
  60. ignition_stack/templates/services/keycloak/compose.yaml.j2 +31 -0
  61. ignition_stack/templates/services/keycloak/manifest.yaml +25 -0
  62. ignition_stack/templates/services/keycloak/seed/service/import/ignition-realm.json +31 -0
  63. ignition_stack/templates/services/mariadb/compose.yaml.j2 +26 -0
  64. ignition_stack/templates/services/mariadb/manifest.yaml +15 -0
  65. ignition_stack/templates/services/mariadb/seed/service/initdb/00-create-extra-databases.sh +19 -0
  66. ignition_stack/templates/services/modbus-sim/compose.yaml.j2 +12 -0
  67. ignition_stack/templates/services/modbus-sim/manifest.yaml +19 -0
  68. ignition_stack/templates/services/modbus-sim/seed/service/USAGE.md +10 -0
  69. ignition_stack/templates/services/mongo/compose.yaml.j2 +21 -0
  70. ignition_stack/templates/services/mongo/manifest.yaml +14 -0
  71. ignition_stack/templates/services/mongo/seed/service/initdb/01-demo-collection.js +10 -0
  72. ignition_stack/templates/services/mysql/compose.yaml.j2 +26 -0
  73. ignition_stack/templates/services/mysql/manifest.yaml +15 -0
  74. ignition_stack/templates/services/mysql/seed/service/initdb/00-create-extra-databases.sh +19 -0
  75. ignition_stack/templates/services/n8n/compose.yaml.j2 +16 -0
  76. ignition_stack/templates/services/n8n/manifest.yaml +16 -0
  77. ignition_stack/templates/services/n8n/seed/service/USAGE.md +11 -0
  78. ignition_stack/templates/services/opcua-sim/compose.yaml.j2 +13 -0
  79. ignition_stack/templates/services/opcua-sim/manifest.yaml +21 -0
  80. ignition_stack/templates/services/opcua-sim/seed/service/USAGE.md +11 -0
  81. ignition_stack/templates/services/postgres/compose.yaml.j2 +26 -0
  82. ignition_stack/templates/services/postgres/manifest.yaml +21 -0
  83. ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/config.json +32 -0
  84. ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/resource.json +19 -0
  85. ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/secret-provider/internal-secret-provider/config.json +19 -0
  86. ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/secret-provider/internal-secret-provider/resource.json +19 -0
  87. ignition_stack/templates/services/postgres/seed/service/initdb/00-create-extra-databases.sh +17 -0
  88. ignition_stack/templates/services/rabbitmq/compose.yaml.j2 +19 -0
  89. ignition_stack/templates/services/rabbitmq/manifest.yaml +23 -0
  90. ignition_stack/templates/services/rabbitmq/seed/service/enabled_plugins +1 -0
  91. ignition_stack/templates/standalone-postgres/docker-compose.yaml +62 -0
  92. ignition_stack/templates/standalone-postgres/scripts/docker-bootstrap.sh +78 -0
  93. ignition_stack/templates/standalone-postgres/services/ignition/config/resources/core/config-mode.json +7 -0
  94. ignition_stack/templates/standalone-postgres/services/ignition/config/resources/dev/config-mode.json +7 -0
  95. ignition_stack/templates/standalone-postgres/services/ignition/projects/.gitkeep +0 -0
  96. ignition_stack/wizard.py +362 -0
  97. ignition_stack-0.1.0.dist-info/METADATA +97 -0
  98. ignition_stack-0.1.0.dist-info/RECORD +100 -0
  99. ignition_stack-0.1.0.dist-info/WHEEL +4 -0
  100. ignition_stack-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,44 @@
1
+ """Standalone profile: one full Ignition gateway + optional SQL DB.
2
+
3
+ This is the Phase-2 walking skeleton's shape, surfaced as a named profile
4
+ so the wizard can offer it alongside the multi-gateway profiles. The only
5
+ knobs are the database choice (defaults to Postgres) and the optional
6
+ reverse-proxy scaffold.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from ignition_stack.config import DatabaseConfig, GatewayConfig, ProjectConfig
14
+ from ignition_stack.profiles.base import Profile, ProfileOptions, register
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class StandaloneProfile:
19
+ slug: str = "standalone"
20
+ summary: str = "One full Ignition 8.3 gateway + Postgres. The default starter stack."
21
+
22
+ def build(self, name: str, options: ProfileOptions) -> ProjectConfig:
23
+ gateway = GatewayConfig()
24
+ if options.edge_role in {"gateway", "standalone"}:
25
+ gateway = gateway.model_copy(update={"ignition_edition": "edge"})
26
+
27
+ return ProjectConfig(
28
+ name=name,
29
+ profile=self.slug,
30
+ gateways=[gateway],
31
+ database=_database(options),
32
+ services=list(options.services),
33
+ reverse_proxy=options.reverse_proxy,
34
+ )
35
+
36
+
37
+ def _database(options: ProfileOptions) -> DatabaseConfig | None:
38
+ if options.database_kind is None:
39
+ return None
40
+ return DatabaseConfig(kind=options.database_kind)
41
+
42
+
43
+ # Side-effect: registers this profile when the module is imported.
44
+ profile: Profile = register(StandaloneProfile())
@@ -0,0 +1,25 @@
1
+ """Service catalog: per-service manifests, loader, and dependency resolver.
2
+
3
+ Phase 5 turns every supported service (databases, MQTT brokers, Keycloak,
4
+ simulators, Kafka, n8n) into a self-contained template directory under
5
+ ``ignition_stack/templates/services/<name>/`` holding a ``manifest.yaml``
6
+ (metadata + capability declarations), a ``compose.yaml.j2`` fragment, and a
7
+ ``seed/`` tree. This package reads those manifests and resolves the implicit
8
+ dependencies between services (Keycloak needs a SQL database; MySQL needs its
9
+ JDBC driver) into a fully-expanded :class:`ProjectConfig` before the compose
10
+ engine renders anything.
11
+ """
12
+
13
+ from ignition_stack.services.loader import load_all_services, load_service, service_dir
14
+ from ignition_stack.services.manifest import PostSetupItem, ServiceManifest
15
+ from ignition_stack.services.resolver import ResolveError, resolve
16
+
17
+ __all__ = [
18
+ "PostSetupItem",
19
+ "ResolveError",
20
+ "ServiceManifest",
21
+ "load_all_services",
22
+ "load_service",
23
+ "resolve",
24
+ "service_dir",
25
+ ]
@@ -0,0 +1,69 @@
1
+ """Load service manifests from the bundled ``templates/services/`` catalog."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from functools import lru_cache
6
+ from importlib import resources
7
+ from importlib.resources.abc import Traversable
8
+
9
+ import yaml
10
+ from pydantic import ValidationError
11
+
12
+ from ignition_stack.services.manifest import ServiceManifest
13
+
14
+ _SERVICES_PACKAGE = "ignition_stack.templates"
15
+ _SERVICES_SUBDIR = "services"
16
+ _MANIFEST_NAME = "manifest.yaml"
17
+
18
+
19
+ class ServiceLoadError(Exception):
20
+ """Raised when a service manifest is missing or fails schema validation."""
21
+
22
+
23
+ def services_root() -> Traversable:
24
+ """The ``templates/services/`` directory inside the installed package."""
25
+ return resources.files(_SERVICES_PACKAGE) / _SERVICES_SUBDIR
26
+
27
+
28
+ def service_dir(name: str) -> Traversable:
29
+ """The ``templates/services/<name>/`` directory for one service."""
30
+ return services_root() / name
31
+
32
+
33
+ def load_service(name: str) -> ServiceManifest:
34
+ """Load and validate one service's manifest by catalog slug."""
35
+ manifest_file = service_dir(name) / _MANIFEST_NAME
36
+ if not manifest_file.is_file():
37
+ raise ServiceLoadError(f"no manifest for service '{name}' (looked for {manifest_file}).")
38
+ try:
39
+ raw = yaml.safe_load(manifest_file.read_text(encoding="utf-8"))
40
+ except yaml.YAMLError as exc:
41
+ raise ServiceLoadError(f"service '{name}' manifest is not valid YAML: {exc}") from exc
42
+ try:
43
+ manifest = ServiceManifest.model_validate(raw)
44
+ except ValidationError as exc:
45
+ raise ServiceLoadError(f"service '{name}' manifest failed validation:\n{exc}") from exc
46
+ if manifest.name != name:
47
+ raise ServiceLoadError(
48
+ f"service '{name}' manifest declares name '{manifest.name}'; they must match."
49
+ )
50
+ return manifest
51
+
52
+
53
+ @lru_cache(maxsize=1)
54
+ def load_all_services() -> dict[str, ServiceManifest]:
55
+ """Load every service manifest, keyed by slug.
56
+
57
+ Cached because the catalog is read-only package data: a single process
58
+ never sees it change. Tests that need a fresh read can call
59
+ ``load_all_services.cache_clear()``.
60
+ """
61
+ catalog: dict[str, ServiceManifest] = {}
62
+ for entry in services_root().iterdir():
63
+ if not entry.is_dir():
64
+ continue
65
+ if not (entry / _MANIFEST_NAME).is_file():
66
+ continue
67
+ manifest = load_service(entry.name)
68
+ catalog[manifest.name] = manifest
69
+ return catalog
@@ -0,0 +1,106 @@
1
+ """Pydantic schema for a service ``manifest.yaml``.
2
+
3
+ Each catalog service ships one manifest describing what the compose engine and
4
+ the dependency resolver need to know about it without reading its Jinja2
5
+ fragment:
6
+
7
+ - **identity** - the catalog slug (also the compose service key and the
8
+ ``templates/services/<name>/`` directory name) and the human kind.
9
+ - **image** - the default ``image:tag`` plus the ``.env`` key that overrides it.
10
+ - **capabilities** - ``provides`` / ``requires`` capability tags the resolver
11
+ uses to wire implicit dependencies (Keycloak ``requires: [sql-database]``).
12
+ - **env** - the preset ``.env`` keys this service contributes, with default
13
+ values, so the ``.env`` writer is data-driven instead of hardcoding each
14
+ service's credentials.
15
+ - **seeding** - whether the service ships a ``seed/gateway-resources/`` tree
16
+ that the writer overlays onto every gateway's file-config resources (per the
17
+ Phase-1 seedability matrix), and which connections it cannot pre-seed and
18
+ must defer to ``POST-SETUP.md`` (Phase 7).
19
+
20
+ The fragment itself references ``${ENV_KEY}`` values directly, so the manifest
21
+ never duplicates the compose body - it only carries the metadata the engine
22
+ cannot infer from YAML text.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Literal
28
+
29
+ from pydantic import BaseModel, ConfigDict, Field
30
+
31
+ # A service either lives on the user-facing ``frontend`` network or the private
32
+ # ``backend`` network when ``network_split`` is on. Databases, brokers, IDPs,
33
+ # simulators, and streaming brokers are backend; the only frontend service is
34
+ # n8n (it exposes a UI users hit directly). Gateways always join both.
35
+ NetworkTier = Literal["frontend", "backend"]
36
+
37
+ ServiceKind = Literal[
38
+ "database",
39
+ "mqtt-broker",
40
+ "idp",
41
+ "simulator",
42
+ "streaming",
43
+ "automation",
44
+ ]
45
+
46
+
47
+ class PostSetupItem(BaseModel):
48
+ """A connection this service cannot fully pre-seed from files.
49
+
50
+ Phase 7's ``POST-SETUP.md`` generator turns each of these into a manual
51
+ step. The ``connection`` matches a row in the Phase-1 seedability matrix;
52
+ ``reason`` quotes why the file-seeding path stops short (usually a secret
53
+ or a handshake the matrix marked ``no`` / ``partial``).
54
+ """
55
+
56
+ model_config = ConfigDict(extra="forbid", frozen=True)
57
+
58
+ connection: str = Field(min_length=1)
59
+ reason: str = Field(min_length=1)
60
+
61
+
62
+ class ServiceManifest(BaseModel):
63
+ """Declarative metadata for one catalog service."""
64
+
65
+ model_config = ConfigDict(extra="forbid", frozen=True)
66
+
67
+ name: str = Field(
68
+ min_length=1,
69
+ pattern=r"^[a-z][a-z0-9-]*$",
70
+ description="Catalog slug; also the compose service key and the directory name.",
71
+ )
72
+ kind: ServiceKind
73
+ summary: str = Field(default="", description="One-line description for docs and headers.")
74
+ image: str = Field(min_length=1, description="Default image:tag (overridable via image_env).")
75
+ image_env: str = Field(
76
+ min_length=1,
77
+ pattern=r"^[A-Z][A-Z0-9_]*$",
78
+ description="The .env key the compose fragment reads for this service's image.",
79
+ )
80
+ network: NetworkTier = Field(
81
+ default="backend",
82
+ description="Which network the service joins when network_split is on.",
83
+ )
84
+ provides: list[str] = Field(
85
+ default_factory=list,
86
+ description="Capability tags this service satisfies (e.g. 'sql-database').",
87
+ )
88
+ requires: list[str] = Field(
89
+ default_factory=list,
90
+ description="Capability tags this service needs; the resolver auto-adds a provider.",
91
+ )
92
+ env: dict[str, str] = Field(
93
+ default_factory=dict,
94
+ description="Preset .env keys -> default values this service contributes.",
95
+ )
96
+ seeds_gateway_resources: bool = Field(
97
+ default=False,
98
+ description=(
99
+ "True when the service ships seed/gateway-resources/ that the writer "
100
+ "overlays onto every gateway's file-config resource tree."
101
+ ),
102
+ )
103
+ post_setup: list[PostSetupItem] = Field(
104
+ default_factory=list,
105
+ description="Connections this service defers to POST-SETUP.md (Phase 7).",
106
+ )
@@ -0,0 +1,133 @@
1
+ """Dependency resolver: expand a user's service selections into a full config.
2
+
3
+ This is a **pure transformation**: given the raw :class:`ProjectConfig` the
4
+ user (or wizard, in Phase 6) built, it returns a new, fully-resolved
5
+ ``ProjectConfig`` with every implicit dependency made explicit. The compose
6
+ engine then renders that resolved config verbatim and never re-resolves at
7
+ render time, which keeps the resolution rules unit-testable in isolation.
8
+
9
+ Two kinds of rule run here, per the hybrid-resolution decision in the design:
10
+
11
+ 1. **Declarative** - each service manifest declares ``requires:`` capability
12
+ tags. If nothing in the stack provides a required capability, the resolver
13
+ auto-adds the default provider (today only databases are auto-addable:
14
+ Keycloak ``requires: [sql-database]`` -> a Postgres database appears).
15
+
16
+ 2. **Imperative** - a small ruleset for couplings that capability tags don't
17
+ express cleanly:
18
+ - Keycloak gets its own logical ``keycloak`` database created on the SQL
19
+ server it lands on.
20
+ - A MySQL database attaches the ``mysql-jdbc`` driver to every gateway so
21
+ the connector ``.jar`` lands in ``user-lib/jdbc/``.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from ignition_stack.config.schema import DatabaseConfig, ProjectConfig
27
+ from ignition_stack.services.loader import load_all_services
28
+ from ignition_stack.services.manifest import ServiceManifest
29
+
30
+
31
+ class ResolveError(Exception):
32
+ """Raised when a selection can't be satisfied (unknown service, DB conflict)."""
33
+
34
+
35
+ # Capability tags each database kind satisfies.
36
+ _DB_CAPABILITIES: dict[str, set[str]] = {
37
+ "postgres": {"sql-database", "postgres-compatible"},
38
+ "mysql": {"sql-database", "mysql-compatible"},
39
+ "mariadb": {"sql-database", "mysql-compatible"},
40
+ "mongo": {"document-store"},
41
+ }
42
+
43
+ # When a required database capability is provided by nothing, add this kind.
44
+ _DEFAULT_DB_FOR_CAPABILITY: dict[str, str] = {
45
+ "sql-database": "postgres",
46
+ "postgres-compatible": "postgres",
47
+ "mysql-compatible": "mysql",
48
+ "document-store": "mongo",
49
+ }
50
+
51
+ # Database kinds whose driver is not built into Ignition and must be attached
52
+ # to gateways as a catalog JDBC entry (slug -> the modules.yaml driver slug).
53
+ _DB_JDBC_DRIVER: dict[str, str] = {"mysql": "mysql-jdbc"}
54
+
55
+ # Database kinds that host a per-application logical database for Keycloak.
56
+ _KEYCLOAK_SQL_KINDS = {"postgres", "mysql", "mariadb"}
57
+
58
+
59
+ def resolve(config: ProjectConfig) -> ProjectConfig:
60
+ """Return a deep copy of ``config`` with implicit dependencies expanded."""
61
+ catalog = load_all_services()
62
+ resolved = config.model_copy(deep=True)
63
+
64
+ _validate_services(resolved, catalog)
65
+ _satisfy_required_capabilities(resolved, catalog)
66
+ _apply_keycloak_database(resolved)
67
+ _apply_jdbc_drivers(resolved)
68
+
69
+ return resolved
70
+
71
+
72
+ def _validate_services(config: ProjectConfig, catalog: dict[str, ServiceManifest]) -> None:
73
+ for svc in config.services:
74
+ if svc not in catalog:
75
+ known = ", ".join(sorted(catalog))
76
+ raise ResolveError(f"unknown service '{svc}'; known services: {known}")
77
+ if catalog[svc].kind == "database":
78
+ raise ResolveError(
79
+ f"'{svc}' is a database; set it as the project's 'database', not in 'services'"
80
+ )
81
+
82
+
83
+ def _satisfy_required_capabilities(
84
+ config: ProjectConfig, catalog: dict[str, ServiceManifest]
85
+ ) -> None:
86
+ required: set[str] = set()
87
+ for svc in config.services:
88
+ required.update(catalog[svc].requires)
89
+
90
+ for cap in sorted(required):
91
+ if _capability_satisfied(cap, config, catalog):
92
+ continue
93
+ db_kind = _DEFAULT_DB_FOR_CAPABILITY.get(cap)
94
+ if db_kind is None:
95
+ raise ResolveError(
96
+ f"required capability '{cap}' is provided by no selected service "
97
+ "and cannot be auto-added"
98
+ )
99
+ if config.database is not None:
100
+ raise ResolveError(
101
+ f"a '{config.database.kind}' database is selected, but capability "
102
+ f"'{cap}' needs a different database and only one database is "
103
+ "supported per stack; choose a compatible database"
104
+ )
105
+ config.database = DatabaseConfig(kind=db_kind)
106
+
107
+
108
+ def _capability_satisfied(
109
+ cap: str, config: ProjectConfig, catalog: dict[str, ServiceManifest]
110
+ ) -> bool:
111
+ if config.database is not None and cap in _DB_CAPABILITIES.get(config.database.kind, set()):
112
+ return True
113
+ return any(cap in catalog[svc].provides for svc in config.services)
114
+
115
+
116
+ def _apply_keycloak_database(config: ProjectConfig) -> None:
117
+ if "keycloak" not in config.services or config.database is None:
118
+ return
119
+ if config.database.kind not in _KEYCLOAK_SQL_KINDS:
120
+ return
121
+ if "keycloak" not in config.database.extra_databases:
122
+ config.database.extra_databases.append("keycloak")
123
+
124
+
125
+ def _apply_jdbc_drivers(config: ProjectConfig) -> None:
126
+ if config.database is None:
127
+ return
128
+ driver = _DB_JDBC_DRIVER.get(config.database.kind)
129
+ if driver is None:
130
+ return
131
+ for gw in config.gateways:
132
+ if driver not in gw.modules:
133
+ gw.modules.append(driver)
File without changes
@@ -0,0 +1,12 @@
1
+ ## Finish the {{ connection.replace('-', ' ') }} connection
2
+
3
+ {{ reason }}
4
+
5
+ - **Open the gateway UI:** {{ gateway_url }}
6
+ - **Navigate to:** Config -> Connections, then open the `{{ connection }}` entry.
7
+ {%- if env_vars %}
8
+ - **Copy from `.env`:**
9
+ {%- for key, value in env_vars %}
10
+ - `{{ key }}` = `{{ value }}`
11
+ {%- endfor %}
12
+ {%- endif %}
@@ -0,0 +1,11 @@
1
+ ## Set up the device connection ({{ service }})
2
+
3
+ {{ reason }}
4
+
5
+ - **Open the gateway UI:** {{ gateway_url }}
6
+ - **Navigate to:** Config -> Device Connections -> Devices -> Create new Device, choose
7
+ the Modbus TCP driver, and point it at `{{ service }}` once the simulator is reachable.
8
+ - **Copy from `.env`:**
9
+ {%- for key, value in env_vars %}
10
+ - `{{ key }}` = `{{ value }}`
11
+ {%- endfor %}
@@ -0,0 +1,18 @@
1
+ ## Approve the gateway-network link
2
+
3
+ {{ reason }}
4
+
5
+ This stack ships two gateways that aggregate over the **gateway-network**. The
6
+ `gateway-network-link` row of the seedability matrix is partial, so approve the
7
+ link by hand:
8
+ {% for gw in gateways %}
9
+ - **{{ gw.role }} gateway** (`{{ gw.name }}`): {{ gw.url }}
10
+ {%- endfor %}
11
+
12
+ 1. Open the **frontend** gateway, go to Config -> Networking -> Gateway Network, and
13
+ add an outgoing connection to `${COMPOSE_PROJECT_NAME}-backend` on port 8060.
14
+ 2. Open the **backend** gateway and approve the incoming connection request.
15
+ - **Copy from `.env`:**
16
+ {%- for key, value in env_vars %}
17
+ - `{{ key }}` = `{{ value }}`
18
+ {%- endfor %}
@@ -0,0 +1,13 @@
1
+ ## Finish the OIDC identity provider (Keycloak)
2
+
3
+ {{ reason }}
4
+
5
+ - **Open Keycloak:** http://localhost:{{ env_map['KEYCLOAK_HTTP_PORT'] }}
6
+ (admin console -> Clients -> `ignition-gateway` -> Credentials -> copy the client secret)
7
+ - **Open the gateway UI:** {{ gateway_url }}
8
+ - **Navigate to:** Config -> Security -> Identity Providers, then add/edit the OIDC IdP.
9
+ - **Copy from `.env`:**
10
+ {%- for key, value in env_vars %}
11
+ - `{{ key }}` = `{{ value }}`
12
+ {%- endfor %}
13
+ - Paste the client secret you copied from Keycloak into the gateway IdP's **Client Secret** field.
@@ -0,0 +1,11 @@
1
+ ## Configure the Kafka connector
2
+
3
+ {{ reason }}
4
+
5
+ - **Open the gateway UI:** {{ gateway_url }}
6
+ - **Navigate to:** Config -> Kafka -> Connectors -> Create new Kafka connector, and
7
+ point its bootstrap servers at `kafka:9092`.
8
+ - **Copy from `.env`:**
9
+ {%- for key, value in env_vars %}
10
+ - `{{ key }}` = `{{ value }}`
11
+ {%- endfor %}
@@ -0,0 +1,11 @@
1
+ ## Drop in the Ignition MCP module
2
+
3
+ {{ reason }}
4
+
5
+ - **Request the module:** https://inductiveautomation.com/early-access (Ignition MCP,
6
+ Early-Access).
7
+ - **Drop the file here:** `{{ dropin_dir }}/<filename>.modl`
8
+ - Re-run `docker compose up -d`; the bootstrap copies any `.modl` in the drop-in dir
9
+ into the gateway's `user-lib/modules/` on startup.
10
+ - **Open the gateway UI:** {{ gateway_url }}
11
+ - **Navigate to:** Config -> Modules to confirm the MCP module loaded.
@@ -0,0 +1,11 @@
1
+ ## Link the gateway to the MQTT broker ({{ service }})
2
+
3
+ {{ reason }}
4
+
5
+ - **Open the gateway UI:** {{ gateway_url }}
6
+ - **Navigate to:** Config -> MQTT Engine -> Servers -> Settings (use MQTT Transmission
7
+ on an edge gateway instead), then add an MQTT server pointing at `{{ service }}`.
8
+ - **Copy from `.env`:**
9
+ {%- for key, value in env_vars %}
10
+ - `{{ key }}` = `{{ value }}`
11
+ {%- endfor %}
@@ -0,0 +1,11 @@
1
+ ## Confirm the OPC-UA connection ({{ service }})
2
+
3
+ {{ reason }}
4
+
5
+ - **Open the gateway UI:** {{ gateway_url }}
6
+ - **Navigate to:** Config -> OPC Connections -> Servers, then open the seeded OPC-UA
7
+ connection and confirm its endpoint URL and security mode against the running simulator.
8
+ - **Copy from `.env`:**
9
+ {%- for key, value in env_vars %}
10
+ - `{{ key }}` = `{{ value }}`
11
+ {%- endfor %}
@@ -0,0 +1,8 @@
1
+ ## Install the reverse proxy
2
+
3
+ {{ reason }}
4
+
5
+ - **Read the scaffolded README:** `{{ proxy_path }}/README.md`
6
+ - Clone ia-eknorr/traefik-reverse-proxy into `{{ proxy_path }}/` and follow its routing
7
+ and TLS instructions to put it in front of the gateway.
8
+ - **Open the gateway UI:** {{ gateway_url }} (today a plain host-port mapping).
@@ -0,0 +1,17 @@
1
+ {{ name }}:
2
+ image: {{ image_ref }}
3
+ hostname: {{ name }}
4
+ container_name: {{ container_name_ref }}
5
+ environment:
6
+ TZ: ${TZ}
7
+ ACCEPT_EULA: "true"
8
+ ADMIN_PASSWORD: ${CHARIOT_ADMIN_PASSWORD}
9
+ ports:
10
+ - "${CHARIOT_HTTP_PORT}:8080"
11
+ - "${CHARIOT_MQTT_PORT}:1883"
12
+ {%- if networks %}
13
+ networks:
14
+ {%- for net in networks %}
15
+ - {{ net }}
16
+ {%- endfor %}
17
+ {%- endif %}
@@ -0,0 +1,22 @@
1
+ # Chariot - the Cirrus Link MQTT broker, the primary pick for Sparkplug SCADA
2
+ # demos. Web UI + MQTT, commissioned from env on first boot.
3
+ name: chariot
4
+ kind: mqtt-broker
5
+ summary: Cirrus Link Chariot MQTT broker (Sparkplug-aware, web UI).
6
+ image: cirruslink/chariot:3.0.0
7
+ image_env: CHARIOT_IMAGE
8
+ network: backend
9
+ provides:
10
+ - mqtt-broker
11
+ requires: []
12
+ env:
13
+ CHARIOT_ADMIN_PASSWORD: password
14
+ CHARIOT_HTTP_PORT: "8090"
15
+ CHARIOT_MQTT_PORT: "1886"
16
+ seeds_gateway_resources: false
17
+ post_setup:
18
+ - connection: mqtt-engine-connection
19
+ reason: >-
20
+ Linking an Ignition gateway to the broker needs the Cirrus Link MQTT
21
+ Engine/Transmission module plus an MQTT server endpoint in the gateway,
22
+ configured once the stack is up.
@@ -0,0 +1,14 @@
1
+ # Chariot MQTT Broker
2
+
3
+ Sparkplug-aware MQTT broker from Cirrus Link, the primary broker for Ignition
4
+ SCADA demos.
5
+
6
+ - Web UI: http://localhost:${CHARIOT_HTTP_PORT} (user `admin`, password
7
+ `${CHARIOT_ADMIN_PASSWORD}`).
8
+ - MQTT from another container: `tcp://chariot:1883`. From the host:
9
+ `tcp://localhost:${CHARIOT_MQTT_PORT}`.
10
+
11
+ The EULA is auto-accepted (`ACCEPT_EULA=true`) so the broker commissions itself
12
+ on first boot. To bridge an Ignition gateway, add the Cirrus Link MQTT
13
+ Transmission/Engine modules and point them at `tcp://chariot:1883`
14
+ (see POST-SETUP.md).
@@ -0,0 +1,16 @@
1
+ {{ name }}:
2
+ image: {{ image_ref }}
3
+ hostname: {{ name }}
4
+ container_name: {{ container_name_ref }}
5
+ environment:
6
+ TZ: ${TZ}
7
+ EMQX_DASHBOARD__DEFAULT_PASSWORD: ${EMQX_DASHBOARD_PASSWORD}
8
+ ports:
9
+ - "${EMQX_MQTT_PORT}:1883"
10
+ - "${EMQX_DASHBOARD_PORT}:18083"
11
+ {%- if networks %}
12
+ networks:
13
+ {%- for net in networks %}
14
+ - {{ net }}
15
+ {%- endfor %}
16
+ {%- endif %}
@@ -0,0 +1,21 @@
1
+ # EMQX - MQTT broker with a web dashboard.
2
+ name: emqx
3
+ kind: mqtt-broker
4
+ summary: EMQX MQTT broker with management dashboard.
5
+ image: emqx/emqx:6.2.0
6
+ image_env: EMQX_IMAGE
7
+ network: backend
8
+ provides:
9
+ - mqtt-broker
10
+ requires: []
11
+ env:
12
+ EMQX_MQTT_PORT: "1884"
13
+ EMQX_DASHBOARD_PORT: "18083"
14
+ EMQX_DASHBOARD_PASSWORD: public
15
+ seeds_gateway_resources: false
16
+ post_setup:
17
+ - connection: mqtt-engine-connection
18
+ reason: >-
19
+ Linking an Ignition gateway to the broker needs the Cirrus Link MQTT
20
+ Engine/Transmission module plus an MQTT server endpoint in the gateway,
21
+ configured once the stack is up.
@@ -0,0 +1,11 @@
1
+ # EMQX
2
+
3
+ MQTT broker on `1883` (host port `${EMQX_MQTT_PORT}`) with a dashboard on
4
+ `18083` (host port `${EMQX_DASHBOARD_PORT}`).
5
+
6
+ - Dashboard: http://localhost:${EMQX_DASHBOARD_PORT} (user `admin`, password
7
+ `${EMQX_DASHBOARD_PASSWORD}`).
8
+ - MQTT from another container: `tcp://emqx:1883`.
9
+
10
+ To bridge an Ignition gateway, add the Cirrus Link MQTT Engine module and point
11
+ its server endpoint at `tcp://emqx:1883` (see POST-SETUP.md).
@@ -0,0 +1,12 @@
1
+ {{ name }}:
2
+ image: {{ image_ref }}
3
+ hostname: {{ name }}
4
+ container_name: {{ container_name_ref }}
5
+ ports:
6
+ - "${HIVEMQ_MQTT_PORT}:1883"
7
+ {%- if networks %}
8
+ networks:
9
+ {%- for net in networks %}
10
+ - {{ net }}
11
+ {%- endfor %}
12
+ {%- endif %}
@@ -0,0 +1,19 @@
1
+ # HiveMQ CE - lightweight MQTT broker, anonymous access by default.
2
+ name: hivemq
3
+ kind: mqtt-broker
4
+ summary: HiveMQ Community Edition MQTT broker.
5
+ image: hivemq/hivemq-ce:2025.5
6
+ image_env: HIVEMQ_IMAGE
7
+ network: backend
8
+ provides:
9
+ - mqtt-broker
10
+ requires: []
11
+ env:
12
+ HIVEMQ_MQTT_PORT: "1883"
13
+ seeds_gateway_resources: false
14
+ post_setup:
15
+ - connection: mqtt-engine-connection
16
+ reason: >-
17
+ Linking an Ignition gateway to the broker needs the Cirrus Link MQTT
18
+ Engine/Transmission module plus an MQTT server endpoint in the gateway,
19
+ configured once the stack is up.
@@ -0,0 +1,16 @@
1
+ # HiveMQ CE
2
+
3
+ Anonymous MQTT broker listening on `1883` (host port `${HIVEMQ_MQTT_PORT}`).
4
+
5
+ From another container on the stack, connect to `tcp://hivemq:1883`. From the
6
+ host, use `tcp://localhost:${HIVEMQ_MQTT_PORT}`.
7
+
8
+ Quick check with the Mosquitto CLI:
9
+
10
+ ```bash
11
+ mosquitto_sub -h localhost -p ${HIVEMQ_MQTT_PORT} -t 'demo/#' &
12
+ mosquitto_pub -h localhost -p ${HIVEMQ_MQTT_PORT} -t 'demo/hello' -m 'from ignition-stack'
13
+ ```
14
+
15
+ To bridge an Ignition gateway to this broker, add the Cirrus Link MQTT Engine
16
+ module and point its server endpoint at `tcp://hivemq:1883` (see POST-SETUP.md).