ignition-stack 0.1.1__tar.gz → 0.2.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.1.1 → ignition_stack-0.2.0}/.gitignore +0 -5
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/PKG-INFO +1 -4
- ignition_stack-0.2.0/ignition_stack/__init__.py +1 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/cli.py +217 -32
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/completion.py +33 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/compose/engine.py +15 -8
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/compose/templates/services/ignition.yaml.j2 +14 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/compose/writer.py +51 -8
- ignition_stack-0.2.0/ignition_stack/config/__init__.py +20 -0
- ignition_stack-0.2.0/ignition_stack/config/io.py +125 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/config/schema.py +84 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/lifecycle/__init__.py +1 -1
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/lifecycle/cleanup.py +3 -2
- ignition_stack-0.2.0/ignition_stack/lifecycle/record.py +73 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/lifecycle/regenerate.py +2 -2
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/postsetup/generator.py +38 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/profiles/__init__.py +4 -0
- ignition_stack-0.2.0/ignition_stack/profiles/base.py +203 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/profiles/hub_and_spoke.py +5 -0
- ignition_stack-0.2.0/ignition_stack/profiles/scaleout.py +80 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/services/resolver.py +53 -1
- ignition_stack-0.2.0/ignition_stack/templates/post-setup/redundancy-pairing.md.j2 +24 -0
- ignition_stack-0.2.0/ignition_stack/templates/redundancy/redundancy.xml.j2 +25 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/standalone-postgres/scripts/docker-bootstrap.sh +9 -1
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/wizard.py +76 -7
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/modules.yaml +1 -1
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/pyproject.toml +0 -5
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/profiles/scaleout/docker-compose.yaml +0 -1
- ignition_stack-0.2.0/tests/golden/profiles/scaleout-redundant/docker-compose.yaml +155 -0
- ignition_stack-0.2.0/tests/test_declarative_io.py +193 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_docs_cli_reference.py +3 -1
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_lifecycle.py +51 -43
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_profiles.py +131 -3
- ignition_stack-0.2.0/tests/test_redundancy.py +267 -0
- ignition_stack-0.2.0/verification/redundancy-spike/README.md +239 -0
- ignition_stack-0.2.0/verification/smoke/README.md +58 -0
- ignition_stack-0.1.1/ignition_stack/__init__.py +0 -1
- ignition_stack-0.1.1/ignition_stack/config/__init__.py +0 -8
- ignition_stack-0.1.1/ignition_stack/lifecycle/record.py +0 -67
- ignition_stack-0.1.1/ignition_stack/profiles/base.py +0 -108
- ignition_stack-0.1.1/ignition_stack/profiles/scaleout.py +0 -65
- ignition_stack-0.1.1/scripts/seeding-poc/README.md +0 -37
- ignition_stack-0.1.1/verification/phase-1-3/README.md +0 -58
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/LICENSE +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/README.md +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/catalog/__init__.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/catalog/download.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/catalog/loader.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/catalog/schema.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/catalog/verify.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/commands/__init__.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/commands/modules.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/compose/__init__.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/compose/templates/footer.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/compose/templates/header.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/compose/templates/services/bootstrap.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/postsetup/__init__.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/profiles/advisory.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/profiles/mcp_n8n.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/profiles/standalone.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/services/__init__.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/services/loader.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/services/manifest.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/__init__.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/post-setup/_default.md.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/post-setup/device-connection.md.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/post-setup/gateway-network-link.md.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/post-setup/identity-provider.md.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/post-setup/kafka-connector.md.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/post-setup/mcp-module.md.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/post-setup/mqtt-engine-connection.md.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/post-setup/opc-ua-connection.md.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/post-setup/reverse-proxy.md.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/chariot/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/chariot/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/chariot/seed/service/USAGE.md +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/emqx/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/emqx/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/emqx/seed/service/USAGE.md +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/hivemq/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/hivemq/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/hivemq/seed/service/USAGE.md +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/kafka/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/kafka/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/kafka/seed/service/USAGE.md +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/keycloak/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/keycloak/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/keycloak/seed/service/import/ignition-realm.json +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/mariadb/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/mariadb/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/mariadb/seed/service/initdb/00-create-extra-databases.sh +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/modbus-sim/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/modbus-sim/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/modbus-sim/seed/service/USAGE.md +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/mongo/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/mongo/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/mongo/seed/service/initdb/01-demo-collection.js +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/mysql/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/mysql/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/mysql/seed/service/initdb/00-create-extra-databases.sh +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/n8n/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/n8n/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/n8n/seed/service/USAGE.md +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/opcua-sim/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/opcua-sim/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/opcua-sim/seed/service/USAGE.md +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/postgres/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/postgres/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/config.json +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/postgres/seed/gateway-resources/config/resources/core/ignition/database-connection/db/resource.json +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.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.1.1 → ignition_stack-0.2.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.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/postgres/seed/service/initdb/00-create-extra-databases.sh +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/rabbitmq/compose.yaml.j2 +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/rabbitmq/manifest.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/services/rabbitmq/seed/service/enabled_plugins +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/standalone-postgres/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/standalone-postgres/services/ignition/config/resources/core/config-mode.json +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/standalone-postgres/services/ignition/config/resources/dev/config-mode.json +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/ignition_stack/templates/standalone-postgres/services/ignition/projects/.gitkeep +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/__init__.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/conftest.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/combos/network-split/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/combos/smoke-stack/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/profiles/hub-and-spoke/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/profiles/mcp-n8n/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/profiles/standalone/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/scaleout-skeleton/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/chariot/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/db-mariadb/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/db-mongo/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/db-mysql/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/db-postgres/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/emqx/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/hivemq/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/kafka/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/keycloak/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/modbus-sim/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/n8n/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/opcua-sim/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/services/rabbitmq/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/golden/standalone-postgres/docker-compose.yaml +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_completion.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_compose_engine.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_init_standalone.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_modules_catalog.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_modules_cli.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_postsetup.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_service_catalog.py +0 -0
- {ignition_stack-0.1.1 → ignition_stack-0.2.0}/tests/test_service_catalog_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ignition-stack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI that generates ready-to-run Docker Compose stacks for Ignition 8.3 SCADA demos and SE engagements
|
|
5
5
|
Author-email: Eric Knorr <etknorr@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -25,9 +25,6 @@ Requires-Dist: typer>=0.12
|
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
27
27
|
Requires-Dist: ruff>=0.7; extra == 'dev'
|
|
28
|
-
Provides-Extra: poc
|
|
29
|
-
Requires-Dist: httpx>=0.27; extra == 'poc'
|
|
30
|
-
Requires-Dist: playwright>=1.49; extra == 'poc'
|
|
31
28
|
Description-Content-Type: text/markdown
|
|
32
29
|
|
|
33
30
|
# ignition-stack
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -13,6 +13,7 @@ interactive wizard when no profile is named.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import subprocess
|
|
16
|
+
from dataclasses import replace
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
|
|
18
19
|
import typer
|
|
@@ -20,9 +21,21 @@ from rich.console import Console
|
|
|
20
21
|
|
|
21
22
|
from ignition_stack import __version__
|
|
22
23
|
from ignition_stack.commands.modules import modules_app
|
|
23
|
-
from ignition_stack.completion import
|
|
24
|
+
from ignition_stack.completion import (
|
|
25
|
+
complete_edge_role,
|
|
26
|
+
complete_output_format,
|
|
27
|
+
complete_profile,
|
|
28
|
+
complete_redundant_role,
|
|
29
|
+
complete_reverse_proxy,
|
|
30
|
+
)
|
|
24
31
|
from ignition_stack.compose import write_project
|
|
25
|
-
from ignition_stack.config import
|
|
32
|
+
from ignition_stack.config import (
|
|
33
|
+
ConfigIOError,
|
|
34
|
+
ProjectConfig,
|
|
35
|
+
ReverseProxyConfig,
|
|
36
|
+
dump_config,
|
|
37
|
+
load_config,
|
|
38
|
+
)
|
|
26
39
|
from ignition_stack.lifecycle import (
|
|
27
40
|
LIFECYCLE_DIR,
|
|
28
41
|
RECORD_NAME,
|
|
@@ -37,9 +50,11 @@ from ignition_stack.profiles import (
|
|
|
37
50
|
ProfileError,
|
|
38
51
|
ProfileOptions,
|
|
39
52
|
build_profile,
|
|
53
|
+
can_host_redundant_role,
|
|
40
54
|
get_profile,
|
|
41
55
|
list_profiles,
|
|
42
56
|
)
|
|
57
|
+
from ignition_stack.services.resolver import resolve
|
|
43
58
|
from ignition_stack.wizard import run_wizard
|
|
44
59
|
|
|
45
60
|
app = typer.Typer(
|
|
@@ -102,6 +117,34 @@ def init(
|
|
|
102
117
|
help="Spoke gateway count for the hub-and-spoke profile (ignored otherwise).",
|
|
103
118
|
min=0,
|
|
104
119
|
),
|
|
120
|
+
frontends: int = typer.Option(
|
|
121
|
+
1,
|
|
122
|
+
"--frontends",
|
|
123
|
+
help="Frontend gateway count for the scaleout profile (ignored otherwise).",
|
|
124
|
+
min=1,
|
|
125
|
+
),
|
|
126
|
+
network_split: bool | None = typer.Option(
|
|
127
|
+
None,
|
|
128
|
+
"--network-split/--no-network-split",
|
|
129
|
+
help=(
|
|
130
|
+
"Force the frontend/backend network split on or off. Default follows "
|
|
131
|
+
"the profile (scaleout splits, hub-and-spoke does not)."
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
reverse_proxy: str | None = typer.Option(
|
|
135
|
+
None,
|
|
136
|
+
"--reverse-proxy",
|
|
137
|
+
help=(
|
|
138
|
+
"Scaffold a reverse proxy of the given kind ('traefik'). Lays down a "
|
|
139
|
+
"README + POST-SETUP entry at --proxy-path. Omit for plain host-port mapping."
|
|
140
|
+
),
|
|
141
|
+
autocompletion=complete_reverse_proxy,
|
|
142
|
+
),
|
|
143
|
+
proxy_path: str = typer.Option(
|
|
144
|
+
"reverse-proxy",
|
|
145
|
+
"--proxy-path",
|
|
146
|
+
help="Relative directory the reverse-proxy scaffold lives in (with --reverse-proxy).",
|
|
147
|
+
),
|
|
105
148
|
force: bool = typer.Option(
|
|
106
149
|
False,
|
|
107
150
|
"--force",
|
|
@@ -111,23 +154,49 @@ def init(
|
|
|
111
154
|
None,
|
|
112
155
|
"--edge-role",
|
|
113
156
|
help=(
|
|
114
|
-
"Gateway role that runs the Ignition Edge edition. Scaleout
|
|
115
|
-
"
|
|
116
|
-
"to disable the profile's edge default; pass a role
|
|
117
|
-
"'gateway', ...) to opt that specific role in."
|
|
157
|
+
"Gateway role that runs the Ignition Edge edition. Scaleout runs all "
|
|
158
|
+
"gateways standard by default; hub-and-spoke defaults its spokes to "
|
|
159
|
+
"Edge. Pass 'none' to disable the profile's edge default; pass a role "
|
|
160
|
+
"name ('frontend', 'hub', 'gateway', ...) to opt that specific role in."
|
|
118
161
|
),
|
|
119
162
|
autocompletion=complete_edge_role,
|
|
120
163
|
),
|
|
121
|
-
|
|
164
|
+
redundant: str | None = typer.Option(
|
|
165
|
+
None,
|
|
166
|
+
"--redundant",
|
|
167
|
+
help=(
|
|
168
|
+
"Make a single gateway role redundant, expanding it into a master + "
|
|
169
|
+
"backup pair (e.g. 'backend' for scaleout, 'hub' for hub-and-spoke, "
|
|
170
|
+
"'gateway' for standalone). Frontends and spokes are replicated, not "
|
|
171
|
+
"paired, and are rejected."
|
|
172
|
+
),
|
|
173
|
+
autocompletion=complete_redundant_role,
|
|
174
|
+
),
|
|
175
|
+
from_file: Path | None = typer.Option( # noqa: B008 - Typer pattern
|
|
176
|
+
None,
|
|
177
|
+
"--from-file",
|
|
178
|
+
"-f",
|
|
179
|
+
help=(
|
|
180
|
+
"Build from a saved config file (YAML or JSON, as dumped by "
|
|
181
|
+
"--dry-run) instead of a profile or the wizard. Mutually exclusive "
|
|
182
|
+
"with --profile. The project name argument overrides the file's name."
|
|
183
|
+
),
|
|
184
|
+
),
|
|
185
|
+
dry_run: bool = typer.Option(
|
|
122
186
|
False,
|
|
123
|
-
"--
|
|
187
|
+
"--dry-run",
|
|
124
188
|
help=(
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"primitives behind."
|
|
189
|
+
"Resolve the config and print it (see --output-format) without "
|
|
190
|
+
"writing any files. The dump is the full build input; redirect it to "
|
|
191
|
+
"a file, edit it, and rebuild with --from-file."
|
|
129
192
|
),
|
|
130
193
|
),
|
|
194
|
+
output_format: str | None = typer.Option(
|
|
195
|
+
None,
|
|
196
|
+
"--output-format",
|
|
197
|
+
help="Format for the --dry-run dump: 'yaml' (default) or 'json'.",
|
|
198
|
+
autocompletion=complete_output_format,
|
|
199
|
+
),
|
|
131
200
|
output_dir: Path | None = typer.Option( # noqa: B008 - Typer pattern
|
|
132
201
|
None,
|
|
133
202
|
"--output-dir",
|
|
@@ -137,11 +206,15 @@ def init(
|
|
|
137
206
|
) -> None:
|
|
138
207
|
"""Generate a new Ignition stack at ``<output-dir>/<name>``.
|
|
139
208
|
|
|
140
|
-
With ``--
|
|
141
|
-
|
|
209
|
+
With ``--from-file``, builds from a saved config file. With ``--profile``,
|
|
210
|
+
runs non-interactively from the named profile and its flags. With neither,
|
|
211
|
+
walks the interactive wizard. ``--dry-run`` resolves the config and prints
|
|
212
|
+
it instead of writing anything.
|
|
142
213
|
"""
|
|
143
214
|
target = ((output_dir or Path.cwd()) / name).resolve()
|
|
144
215
|
|
|
216
|
+
_validate_init_flags(profile=profile, from_file=from_file, dry_run=dry_run, fmt=output_format)
|
|
217
|
+
|
|
145
218
|
# Name validation runs before either the wizard or the profile build so
|
|
146
219
|
# invalid names fail fast with a clear exit code (2), instead of bubbling
|
|
147
220
|
# through the wizard's first prompt or the profile's deep model_validate.
|
|
@@ -151,19 +224,38 @@ def init(
|
|
|
151
224
|
console.print(f"[red]error[/red]: invalid project name: {exc}")
|
|
152
225
|
raise typer.Exit(code=2) from exc
|
|
153
226
|
|
|
154
|
-
if
|
|
227
|
+
if from_file is not None:
|
|
228
|
+
config = _load_from_file(from_file, name)
|
|
229
|
+
elif profile is None:
|
|
155
230
|
config = _run_wizard_or_exit(name)
|
|
156
231
|
else:
|
|
157
|
-
config = _build_from_profile(
|
|
232
|
+
config = _build_from_profile(
|
|
233
|
+
name,
|
|
234
|
+
profile,
|
|
235
|
+
spokes=spokes,
|
|
236
|
+
frontends=frontends,
|
|
237
|
+
force=force,
|
|
238
|
+
edge_role=edge_role,
|
|
239
|
+
network_split=network_split,
|
|
240
|
+
reverse_proxy=reverse_proxy,
|
|
241
|
+
proxy_path=proxy_path,
|
|
242
|
+
redundant=redundant,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if dry_run:
|
|
246
|
+
# Dump the resolved config (the writer resolves too, so this is exactly
|
|
247
|
+
# what would be built) and write nothing. `end=""`/`markup=False` keep
|
|
248
|
+
# the output verbatim and parseable - no rich markup, no extra newline.
|
|
249
|
+
console.print(dump_config(resolve(config), output_format or "yaml"), end="", markup=False)
|
|
250
|
+
raise typer.Exit()
|
|
158
251
|
|
|
159
252
|
try:
|
|
160
|
-
files = write_project(config, target
|
|
253
|
+
files = write_project(config, target)
|
|
161
254
|
except FileExistsError as exc:
|
|
162
255
|
console.print(f"[red]error[/red]: {exc}")
|
|
163
256
|
raise typer.Exit(code=1) from exc
|
|
164
257
|
|
|
165
|
-
|
|
166
|
-
console.print(f"[green]created[/green] {target} ([cyan]{mode}[/cyan])")
|
|
258
|
+
console.print(f"[green]created[/green] {target}")
|
|
167
259
|
console.print(f" {len(files)} file(s) written")
|
|
168
260
|
console.print()
|
|
169
261
|
console.print("Next steps:")
|
|
@@ -172,16 +264,70 @@ def init(
|
|
|
172
264
|
console.print(
|
|
173
265
|
f" open http://localhost:{config.gateways[0].http_port} (admin / {config.admin_password})"
|
|
174
266
|
)
|
|
175
|
-
|
|
176
|
-
|
|
267
|
+
console.print()
|
|
268
|
+
console.print(
|
|
269
|
+
f" config recorded in {LIFECYCLE_DIR}/ - run `ignition-stack reset` to "
|
|
270
|
+
"regenerate or `switch-profile <name>` to reshape this stack."
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _validate_init_flags(
|
|
275
|
+
*, profile: str | None, from_file: Path | None, dry_run: bool, fmt: str | None
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Enforce the mutual-exclusion + flag-applicability rules, or exit code 2.
|
|
278
|
+
|
|
279
|
+
``--from-file`` already fully specifies the topology, so combining it with
|
|
280
|
+
``--profile`` is ambiguous and rejected. ``--output-format`` only shapes the
|
|
281
|
+
``--dry-run`` dump, so passing it without ``--dry-run`` is a usage error
|
|
282
|
+
rather than a silent no-op. The value itself is validated against the two
|
|
283
|
+
supported formats here so a bad ``--output-format`` fails before any build.
|
|
284
|
+
"""
|
|
285
|
+
if from_file is not None and profile is not None:
|
|
177
286
|
console.print(
|
|
178
|
-
|
|
179
|
-
"
|
|
287
|
+
"[red]error[/red]: --from-file cannot be combined with --profile; a "
|
|
288
|
+
"config file already specifies the full topology."
|
|
180
289
|
)
|
|
290
|
+
raise typer.Exit(code=2)
|
|
291
|
+
if fmt is not None and not dry_run:
|
|
292
|
+
console.print("[red]error[/red]: --output-format only applies with --dry-run.")
|
|
293
|
+
raise typer.Exit(code=2)
|
|
294
|
+
if fmt is not None and fmt not in {"yaml", "json"}:
|
|
295
|
+
console.print(
|
|
296
|
+
f"[red]error[/red]: unsupported --output-format '{fmt}'; use 'yaml' or 'json'."
|
|
297
|
+
)
|
|
298
|
+
raise typer.Exit(code=2)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _load_from_file(from_file: Path, name: str) -> ProjectConfig:
|
|
302
|
+
"""Load a config file, override its name with the CLI argument, or exit cleanly.
|
|
303
|
+
|
|
304
|
+
The project-name argument wins over the file's ``name`` so the same dumped
|
|
305
|
+
config can be rebuilt under a new name; everything else comes from the file.
|
|
306
|
+
A parse or validation failure surfaces as a readable error (exit code 2),
|
|
307
|
+
never a traceback.
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
config = load_config(from_file)
|
|
311
|
+
except ConfigIOError as exc:
|
|
312
|
+
console.print(f"[red]error[/red]: {exc}")
|
|
313
|
+
raise typer.Exit(code=2) from exc
|
|
314
|
+
if config.name != name:
|
|
315
|
+
config = config.model_copy(update={"name": name})
|
|
316
|
+
return config
|
|
181
317
|
|
|
182
318
|
|
|
183
319
|
def _build_from_profile(
|
|
184
|
-
name: str,
|
|
320
|
+
name: str,
|
|
321
|
+
profile: str,
|
|
322
|
+
*,
|
|
323
|
+
spokes: int,
|
|
324
|
+
frontends: int,
|
|
325
|
+
force: bool,
|
|
326
|
+
edge_role: str | None,
|
|
327
|
+
network_split: bool | None,
|
|
328
|
+
reverse_proxy: str | None,
|
|
329
|
+
proxy_path: str,
|
|
330
|
+
redundant: str | None,
|
|
185
331
|
) -> ProjectConfig:
|
|
186
332
|
"""Materialize a config from the named profile + CLI flags, or exit cleanly."""
|
|
187
333
|
try:
|
|
@@ -190,7 +336,16 @@ def _build_from_profile(
|
|
|
190
336
|
console.print(f"[red]error[/red]: {exc}")
|
|
191
337
|
raise typer.Exit(code=2) from exc
|
|
192
338
|
|
|
193
|
-
|
|
339
|
+
proxy = ReverseProxyConfig(kind=reverse_proxy, path=proxy_path) if reverse_proxy else None
|
|
340
|
+
options = ProfileOptions(
|
|
341
|
+
spokes=spokes,
|
|
342
|
+
frontends=frontends,
|
|
343
|
+
force=force,
|
|
344
|
+
edge_role=edge_role,
|
|
345
|
+
network_split=network_split,
|
|
346
|
+
reverse_proxy=proxy,
|
|
347
|
+
redundant_role=redundant,
|
|
348
|
+
)
|
|
194
349
|
try:
|
|
195
350
|
config = build_profile(profile, name, options)
|
|
196
351
|
except ProfileError as exc:
|
|
@@ -220,14 +375,14 @@ def reset(
|
|
|
220
375
|
Path("."),
|
|
221
376
|
"--project-dir",
|
|
222
377
|
"-C",
|
|
223
|
-
help="The generated
|
|
378
|
+
help="The generated project to reset. Defaults to the current directory.",
|
|
224
379
|
),
|
|
225
380
|
) -> None:
|
|
226
|
-
"""Regenerate
|
|
381
|
+
"""Regenerate a project from its recorded config.
|
|
227
382
|
|
|
228
383
|
Reads ``.ignition-stack/config.json``, clears the generated tree (keeping the
|
|
229
|
-
record and the modules cache), and re-runs generation.
|
|
230
|
-
|
|
384
|
+
record and the modules cache), and re-runs generation. Works on any project
|
|
385
|
+
generated by this CLI; a directory without a record can't be reset.
|
|
231
386
|
"""
|
|
232
387
|
project_dir = project_dir.resolve()
|
|
233
388
|
try:
|
|
@@ -252,10 +407,10 @@ def switch_profile(
|
|
|
252
407
|
Path("."),
|
|
253
408
|
"--project-dir",
|
|
254
409
|
"-C",
|
|
255
|
-
help="The generated
|
|
410
|
+
help="The generated project to reshape. Defaults to the current directory.",
|
|
256
411
|
),
|
|
257
412
|
) -> None:
|
|
258
|
-
"""Reshape
|
|
413
|
+
"""Reshape a project under a different architecture profile.
|
|
259
414
|
|
|
260
415
|
Carries the recorded database, services, reverse-proxy, and edge intent over
|
|
261
416
|
to the new profile, then regenerates in place and re-records the result.
|
|
@@ -274,6 +429,19 @@ def switch_profile(
|
|
|
274
429
|
raise typer.Exit(code=2) from exc
|
|
275
430
|
|
|
276
431
|
options = _options_from_config(current)
|
|
432
|
+
# Redundancy is pinned to a profile-specific role (e.g. standalone's
|
|
433
|
+
# 'gateway'), which the target profile may not have. Building its base
|
|
434
|
+
# topology lets us check before build_profile's mark_redundant would reject
|
|
435
|
+
# it - drop the intent with an advisory rather than failing the reshape.
|
|
436
|
+
if options.redundant_role is not None and not can_host_redundant_role(
|
|
437
|
+
get_profile(profile).build(current.name, options), options.redundant_role
|
|
438
|
+
):
|
|
439
|
+
console.print(
|
|
440
|
+
f"[yellow]note[/yellow]: redundancy on '{options.redundant_role}' was not "
|
|
441
|
+
f"carried to {profile} (no matching gateway); re-apply with --redundant "
|
|
442
|
+
"if the new topology has a role to pair"
|
|
443
|
+
)
|
|
444
|
+
options = replace(options, redundant_role=None)
|
|
277
445
|
try:
|
|
278
446
|
new_config = build_profile(profile, current.name, options)
|
|
279
447
|
except ProfileError as exc:
|
|
@@ -293,16 +461,33 @@ def _options_from_config(config: ProjectConfig) -> ProfileOptions:
|
|
|
293
461
|
|
|
294
462
|
Edge intent is recovered from whichever gateway runs the Edge edition (or
|
|
295
463
|
'none' to keep the new profile from re-introducing its edge default); the
|
|
296
|
-
spoke count from the number of spoke-role gateways
|
|
464
|
+
spoke count from the number of spoke-role gateways, the frontend count from
|
|
465
|
+
the number of frontend-role gateways, and the network split is carried over
|
|
466
|
+
verbatim so a reshape preserves the user's topology choice.
|
|
297
467
|
"""
|
|
298
468
|
edge_roles = [gw.role or gw.name for gw in config.gateways if gw.ignition_edition == "edge"]
|
|
299
469
|
spoke_count = sum(1 for gw in config.gateways if (gw.role or "") == "spoke")
|
|
470
|
+
frontend_count = sum(1 for gw in config.gateways if (gw.role or "") == "frontend")
|
|
471
|
+
# Redundancy intent is carried by the master node (the backup is re-derived
|
|
472
|
+
# by the resolver), so recover the role/name of whichever gateway is the
|
|
473
|
+
# master and let the new profile re-expand the pair.
|
|
474
|
+
redundant_role = next(
|
|
475
|
+
(
|
|
476
|
+
gw.role or gw.name
|
|
477
|
+
for gw in config.gateways
|
|
478
|
+
if gw.redundancy is not None and gw.redundancy.mode == "master"
|
|
479
|
+
),
|
|
480
|
+
None,
|
|
481
|
+
)
|
|
300
482
|
return ProfileOptions(
|
|
301
483
|
spokes=spoke_count or 3,
|
|
484
|
+
frontends=frontend_count or 1,
|
|
302
485
|
edge_role=edge_roles[0] if edge_roles else "none",
|
|
486
|
+
network_split=config.network_split,
|
|
303
487
|
reverse_proxy=config.reverse_proxy,
|
|
304
488
|
database_kind=config.database.kind if config.database else None,
|
|
305
489
|
services=tuple(config.services),
|
|
490
|
+
redundant_role=redundant_role,
|
|
306
491
|
)
|
|
307
492
|
|
|
308
493
|
|
|
@@ -33,6 +33,39 @@ def complete_edge_role(incomplete: str) -> list[str]:
|
|
|
33
33
|
return [role for role in EDGE_ROLE_VALUES if role.startswith(incomplete)]
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
# Reverse-proxy kinds the CLI can scaffold. Mirrors ReverseProxyConfig.kind
|
|
37
|
+
# (only Traefik exists today); kept here as the completion vocabulary since
|
|
38
|
+
# the Literal lives in the pydantic model, not a runtime registry.
|
|
39
|
+
REVERSE_PROXY_VALUES = ("traefik",)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def complete_reverse_proxy(incomplete: str) -> list[str]:
|
|
43
|
+
"""Reverse-proxy kind names matching the typed prefix."""
|
|
44
|
+
return [kind for kind in REVERSE_PROXY_VALUES if kind.startswith(incomplete)]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Roles `init --redundant` can pair. Only the singleton workhorse roles are
|
|
48
|
+
# eligible (a scaleout 'backend', a hub-and-spoke 'hub', a standalone
|
|
49
|
+
# 'gateway'); replicated 'frontend'/'spoke' tiers are rejected by the profile
|
|
50
|
+
# builder, so they are intentionally absent here.
|
|
51
|
+
REDUNDANT_ROLE_VALUES = ("backend", "hub", "gateway")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def complete_redundant_role(incomplete: str) -> list[str]:
|
|
55
|
+
"""Redundancy-eligible role names matching the typed prefix."""
|
|
56
|
+
return [role for role in REDUNDANT_ROLE_VALUES if role.startswith(incomplete)]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Serialization formats `init --dry-run --output-format` accepts. Mirrors the
|
|
60
|
+
# `Format` literal in config/io.py; kept here as the completion vocabulary.
|
|
61
|
+
OUTPUT_FORMAT_VALUES = ("yaml", "json")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def complete_output_format(incomplete: str) -> list[str]:
|
|
65
|
+
"""Config dump format names matching the typed prefix."""
|
|
66
|
+
return [fmt for fmt in OUTPUT_FORMAT_VALUES if fmt.startswith(incomplete)]
|
|
67
|
+
|
|
68
|
+
|
|
36
69
|
def complete_module_name(incomplete: str) -> list[str]:
|
|
37
70
|
"""Catalog entry slugs from the bundled catalog matching the typed prefix."""
|
|
38
71
|
try:
|
|
@@ -261,6 +261,16 @@ def _ignition_context(
|
|
|
261
261
|
# override when this gateway differs - keeps Phase 2's environment
|
|
262
262
|
# block as the bare anchor reference.
|
|
263
263
|
edition_override = gw.ignition_edition if gw.ignition_edition != "standard" else None
|
|
264
|
+
|
|
265
|
+
# Redundancy wiring (Phase 4, per the verified Phase-3 spike):
|
|
266
|
+
# - Any node in a pair opens its incoming Gateway Network policy
|
|
267
|
+
# (Unrestricted + no-SSL) so the plain redundancy link auto-approves.
|
|
268
|
+
# - The backup additionally points a generic outgoing GAN connection at
|
|
269
|
+
# the master (HOST/PORT/ENABLESSL - all three, not just HOST, or it
|
|
270
|
+
# defaults to SSL:8060 and faults) and must NOT be renamed via -n: it
|
|
271
|
+
# adopts the master's system name on first sync.
|
|
272
|
+
is_redundant = gw.redundancy is not None
|
|
273
|
+
is_backup = is_redundant and gw.redundancy.mode == "backup"
|
|
264
274
|
return {
|
|
265
275
|
"service_name": ctx["service_name"],
|
|
266
276
|
"bootstrap_service_name": ctx["bootstrap_service_name"],
|
|
@@ -272,6 +282,10 @@ def _ignition_context(
|
|
|
272
282
|
"module_identifiers": ctx["module_identifiers"],
|
|
273
283
|
"database_service": config.database.name if config.database else None,
|
|
274
284
|
"networks": ctx["networks"],
|
|
285
|
+
"redundant": is_redundant,
|
|
286
|
+
"rename": not is_backup,
|
|
287
|
+
"gan_peer_host": gw.redundancy.peer if is_backup else None,
|
|
288
|
+
"gan_port": gw.redundancy.gan_port if is_backup else None,
|
|
275
289
|
}
|
|
276
290
|
|
|
277
291
|
|
|
@@ -332,12 +346,7 @@ def _wrap_description(description: str) -> list[str]:
|
|
|
332
346
|
def _describe(config: ProjectConfig) -> str:
|
|
333
347
|
"""Human-readable header comment summarizing the stack."""
|
|
334
348
|
n = len(config.gateways)
|
|
335
|
-
if
|
|
336
|
-
n == 1
|
|
337
|
-
and config.database
|
|
338
|
-
and config.database.kind == "postgres"
|
|
339
|
-
and not config.services
|
|
340
|
-
):
|
|
349
|
+
if n == 1 and config.database and config.database.kind == "postgres" and not config.services:
|
|
341
350
|
return (
|
|
342
351
|
"Walking skeleton: one Ignition 8.3 gateway, one Postgres, "
|
|
343
352
|
"env-driven commissioning so first boot needs no UI."
|
|
@@ -393,5 +402,3 @@ def _round_trip(raw: str) -> str:
|
|
|
393
402
|
out = io.StringIO()
|
|
394
403
|
yaml.dump(parsed, out)
|
|
395
404
|
return out.getvalue()
|
|
396
|
-
|
|
397
|
-
|
|
@@ -21,9 +21,23 @@
|
|
|
21
21
|
{%- if module_identifiers %}
|
|
22
22
|
ACCEPT_MODULE_LICENSES: "{{ module_identifiers }}"
|
|
23
23
|
ACCEPT_MODULE_CERTS: "{{ module_identifiers }}"
|
|
24
|
+
{%- endif %}
|
|
25
|
+
{%- if redundant %}
|
|
26
|
+
GATEWAY_NETWORK_ENABLED: "true"
|
|
27
|
+
GATEWAY_NETWORK_ALLOWINCOMING: "true"
|
|
28
|
+
GATEWAY_NETWORK_SECURITYPOLICY: "Unrestricted"
|
|
29
|
+
GATEWAY_NETWORK_REQUIRESSL: "false"
|
|
30
|
+
GATEWAY_NETWORK_REQUIRETWOWAYAUTH: "false"
|
|
31
|
+
{%- endif %}
|
|
32
|
+
{%- if gan_peer_host %}
|
|
33
|
+
GATEWAY_NETWORK_0_HOST: "{{ gan_peer_host }}"
|
|
34
|
+
GATEWAY_NETWORK_0_PORT: "{{ gan_port }}"
|
|
35
|
+
GATEWAY_NETWORK_0_ENABLESSL: "false"
|
|
24
36
|
{%- endif %}
|
|
25
37
|
command: >
|
|
38
|
+
{%- if rename %}
|
|
26
39
|
-n {{ gateway_name_ref }}
|
|
40
|
+
{%- endif %}
|
|
27
41
|
-m {{ memory_mb }}
|
|
28
42
|
--
|
|
29
43
|
-Dignition.config.mode=dev
|
|
@@ -24,6 +24,8 @@ from importlib import resources
|
|
|
24
24
|
from importlib.resources.abc import Traversable
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
|
|
27
|
+
from jinja2 import Environment, PackageLoader, StrictUndefined
|
|
28
|
+
|
|
27
29
|
from ignition_stack.catalog.loader import CatalogLoadError, load_catalog
|
|
28
30
|
from ignition_stack.catalog.schema import Catalog
|
|
29
31
|
from ignition_stack.compose.engine import render_compose
|
|
@@ -41,7 +43,6 @@ def write_project(
|
|
|
41
43
|
config: ProjectConfig,
|
|
42
44
|
target_dir: Path,
|
|
43
45
|
*,
|
|
44
|
-
keep_cli: bool = False,
|
|
45
46
|
overwrite: bool = False,
|
|
46
47
|
) -> list[Path]:
|
|
47
48
|
"""Generate the project tree at ``target_dir``.
|
|
@@ -50,9 +51,10 @@ def write_project(
|
|
|
50
51
|
:func:`ignition_stack.services.resolver.resolve`) so the compose output and
|
|
51
52
|
the on-disk seeds agree on the same fully-expanded stack.
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
Every project records its resolved config under ``.ignition-stack/`` so
|
|
55
|
+
``reset`` / ``switch-profile`` can regenerate or reshape it in place; the
|
|
56
|
+
same artifact can be dumped with ``init --dry-run`` and rebuilt with
|
|
57
|
+
``init -f``.
|
|
56
58
|
|
|
57
59
|
``overwrite`` lets ``reset`` / ``switch-profile`` write into a directory
|
|
58
60
|
that still holds the preserved ``.ignition-stack/`` record; normal ``init``
|
|
@@ -76,6 +78,7 @@ def write_project(
|
|
|
76
78
|
written.extend(_copy_static_tree(config, target_dir))
|
|
77
79
|
written.extend(_copy_service_seeds(config, target_dir))
|
|
78
80
|
written.extend(_overlay_gateway_resources(config, target_dir))
|
|
81
|
+
written.extend(_write_redundancy_seeds(config, target_dir))
|
|
79
82
|
_ensure_modules_cache_dir(config, target_dir)
|
|
80
83
|
written.append(_write_compose(config, target_dir))
|
|
81
84
|
written.append(_write_env(config, target_dir))
|
|
@@ -88,9 +91,7 @@ def write_project(
|
|
|
88
91
|
if dropin_file is not None:
|
|
89
92
|
written.append(dropin_file)
|
|
90
93
|
written.append(_write_post_setup(config, target_dir))
|
|
91
|
-
|
|
92
|
-
if keep_cli:
|
|
93
|
-
written.append(write_record(config, target_dir))
|
|
94
|
+
written.append(write_record(config, target_dir))
|
|
94
95
|
|
|
95
96
|
return written
|
|
96
97
|
|
|
@@ -138,6 +139,48 @@ def _overlay_gateway_resources(config: ProjectConfig, target_dir: Path) -> list[
|
|
|
138
139
|
return written
|
|
139
140
|
|
|
140
141
|
|
|
142
|
+
def _write_redundancy_seeds(config: ProjectConfig, target_dir: Path) -> list[Path]:
|
|
143
|
+
"""Drop a per-node ``redundancy.xml`` into each redundant gateway's tree.
|
|
144
|
+
|
|
145
|
+
Per the Phase-3 spike, nothing sets the redundancy *role* via env var, so a
|
|
146
|
+
pre-seeded ``data/redundancy.xml`` is what makes a node a master or backup.
|
|
147
|
+
The file lands at ``services/<gateway>/redundancy.xml`` (the gateway's
|
|
148
|
+
template-source root); the bootstrap copies it to the data-volume root on
|
|
149
|
+
first boot. Master and backup differ only in ``noderole`` and ``gan.host``;
|
|
150
|
+
the backup points its host at the master's service name. Generated stacks
|
|
151
|
+
use the plain (non-SSL) link on port 8088, which auto-approves without the
|
|
152
|
+
certificate handshake the SSL path (8060) would force.
|
|
153
|
+
"""
|
|
154
|
+
env = _redundancy_jinja_env()
|
|
155
|
+
template = env.get_template("redundancy.xml.j2")
|
|
156
|
+
written: list[Path] = []
|
|
157
|
+
for gw in config.gateways:
|
|
158
|
+
red = gw.redundancy
|
|
159
|
+
if red is None or not red.seed_redundancy_xml:
|
|
160
|
+
continue
|
|
161
|
+
is_master = red.mode == "master"
|
|
162
|
+
rendered = template.render(
|
|
163
|
+
noderole="Master" if is_master else "Backup",
|
|
164
|
+
# The master listens; the backup connects to the master's service
|
|
165
|
+
# name. An empty host on the master matches the verified seed file.
|
|
166
|
+
gan_host="" if is_master else red.peer,
|
|
167
|
+
gan_port=red.gan_port,
|
|
168
|
+
enable_ssl="true" if red.gan_port == 8060 else "false",
|
|
169
|
+
)
|
|
170
|
+
rel = f"services/{gw.name}/redundancy.xml"
|
|
171
|
+
written.append(_write_static(target_dir, rel, rendered.encode(), False))
|
|
172
|
+
return written
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _redundancy_jinja_env() -> Environment:
|
|
176
|
+
return Environment(
|
|
177
|
+
loader=PackageLoader("ignition_stack.templates", "redundancy"),
|
|
178
|
+
undefined=StrictUndefined,
|
|
179
|
+
keep_trailing_newline=True,
|
|
180
|
+
autoescape=False,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
141
184
|
def _seed_sources(config: ProjectConfig) -> list[tuple[object, str]]:
|
|
142
185
|
"""(service-catalog dir, on-disk destination name) for the DB + each service.
|
|
143
186
|
|
|
@@ -378,7 +421,7 @@ logs: ## Follow logs for every service.
|
|
|
378
421
|
wipe: ## Remove ONLY this project's containers, networks, and volumes.
|
|
379
422
|
\t$(COMPOSE) -p $(PROJECT) down -v --remove-orphans
|
|
380
423
|
|
|
381
|
-
reset: ## Regenerate this project from its recorded
|
|
424
|
+
reset: ## Regenerate this project from its recorded config.
|
|
382
425
|
\tignition-stack reset
|
|
383
426
|
"""
|
|
384
427
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from ignition_stack.config.io import ConfigIOError, Format, dump_config, load_config
|
|
2
|
+
from ignition_stack.config.schema import (
|
|
3
|
+
DatabaseConfig,
|
|
4
|
+
GatewayConfig,
|
|
5
|
+
ProjectConfig,
|
|
6
|
+
RedundancyConfig,
|
|
7
|
+
ReverseProxyConfig,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ConfigIOError",
|
|
12
|
+
"DatabaseConfig",
|
|
13
|
+
"Format",
|
|
14
|
+
"GatewayConfig",
|
|
15
|
+
"ProjectConfig",
|
|
16
|
+
"RedundancyConfig",
|
|
17
|
+
"ReverseProxyConfig",
|
|
18
|
+
"dump_config",
|
|
19
|
+
"load_config",
|
|
20
|
+
]
|