diff --git a/testing/chaos/sim/compose.py b/testing/chaos/sim/compose.py index 5eec26b..c27a153 100644 --- a/testing/chaos/sim/compose.py +++ b/testing/chaos/sim/compose.py @@ -47,7 +47,7 @@ services: {% for node in nodes %} {{ node.node_id }}: <<: *fips-common - container_name: fips-node-{{ node.node_id }} + container_name: {{ topology.container_name(node.node_id) }} hostname: {{ node.node_id }} volumes: - ./{{ node.node_id }}.yaml:/etc/fips/fips.yaml:ro @@ -82,6 +82,7 @@ def generate_compose( image=FIPS_SIM_IMAGE, nodes=nodes, resolv_conf=resolv_conf, + topology=topology, ) path = os.path.join(output_dir, "docker-compose.yml") diff --git a/testing/chaos/sim/naming.py b/testing/chaos/sim/naming.py new file mode 100644 index 0000000..d3418b1 --- /dev/null +++ b/testing/chaos/sim/naming.py @@ -0,0 +1,17 @@ +"""Scoping suffix for names that live in a global namespace.""" + +from __future__ import annotations + +import os + + +def name_suffix() -> str: + """Return the suffix appended to globally-scoped names. + + Docker container names and the generated-config directory are shared + across every simulation running on the host, so the harness exports + ``FIPS_CI_NAME_SUFFIX`` to keep concurrent runs and concurrent + scenarios apart. The suffix is empty when the variable is unset, so a + bare ``chaos.sh`` run renders exactly the same names as it always has. + """ + return os.environ.get("FIPS_CI_NAME_SUFFIX", "") diff --git a/testing/chaos/sim/runner.py b/testing/chaos/sim/runner.py index dc4960b..056f359 100644 --- a/testing/chaos/sim/runner.py +++ b/testing/chaos/sim/runner.py @@ -133,9 +133,17 @@ class SimRunner: log.info(" %s: peers=%s", nid, ",".join(peers)) # 2. Generate configs + # + # The directory carries the same suffix as the container names, so + # parallel scenarios cannot overwrite each other's compose file + # between writing it and starting containers from it. docker_network_dir = os.path.join(os.path.dirname(__file__), "..") config_dir = os.path.normpath( - os.path.join(docker_network_dir, "generated-configs", "sim") + os.path.join( + docker_network_dir, + "generated-configs", + f"sim{self.topology.name_suffix}", + ) ) # Select ephemeral identity nodes (if peer churn enabled) self._ephemeral_nodes: set[str] = set() diff --git a/testing/chaos/sim/topology.py b/testing/chaos/sim/topology.py index e2e1ab6..c8b5d9c 100644 --- a/testing/chaos/sim/topology.py +++ b/testing/chaos/sim/topology.py @@ -8,6 +8,7 @@ from collections import deque from dataclasses import dataclass, field from .keys import derive +from .naming import name_suffix from .scenario import TopologyConfig @@ -28,6 +29,9 @@ class SimTopology: edges: set[tuple[str, str]] = field(default_factory=set) # Per-edge transport type; edges not in this dict default to "udp" edge_transport: dict[tuple[str, str], str] = field(default_factory=dict) + # Suffix scoping globally-visible names to this run and scenario; empty + # outside the CI harness, which keeps a bare run's names unchanged. + name_suffix: str = "" def transport_for_edge(self, a: str, b: str) -> str: """Get the transport type for an edge (defaults to 'udp').""" @@ -107,7 +111,7 @@ class SimTopology: return not connected def container_name(self, node_id: str) -> str: - return f"fips-node-{node_id}" + return f"fips-node-{node_id}{self.name_suffix}" def directed_outbound(self) -> dict[str, list[str]]: """Assign each static-config edge to exactly one node for outbound connection. @@ -219,7 +223,14 @@ def generate_topology( nodes[a].peers.append(b) nodes[b].peers.append(a) - topo = SimTopology(nodes=nodes, edges=edges, edge_transport=edge_transport) + # Read the environment once, here, so every name a run produces comes + # from the same value. + topo = SimTopology( + nodes=nodes, + edges=edges, + edge_transport=edge_transport, + name_suffix=name_suffix(), + ) # Connectivity check with retry if config.ensure_connected: diff --git a/testing/ci-local.sh b/testing/ci-local.sh index 61917df..d639f74 100755 --- a/testing/ci-local.sh +++ b/testing/ci-local.sh @@ -516,9 +516,16 @@ run_chaos() { local name="$1" shift local rc=0 - # Distinct project per scenario (chaos children run in parallel). When - # invoked from a background subshell this export is local to that child. - export COMPOSE_PROJECT_NAME="$(ci_project "chaos-$name")" + # Distinct project per scenario (chaos children run in parallel). Scoped to + # this function so the --only path cannot leak it into a later suite. + local -x COMPOSE_PROJECT_NAME="$(ci_project "chaos-$name")" + + # Container names and the generated-config directory are GLOBAL and are not + # scoped by the compose project, so narrow the run-wide suffix to this + # scenario. Parallel children then cannot claim each other's names or + # overwrite each other's compose file. + local suffix="-${name}${FIPS_CI_NAME_SUFFIX:-}" + local -x FIPS_CI_NAME_SUFFIX="$suffix" info "[chaos/$name] Running simulation" if bash testing/chaos/scripts/chaos.sh "$@" 2>&1; then