Give each chaos scenario its own container names and config directory

Chaos scenarios run four at a time under local CI, but every one of them claimed the container names fips-node-nNN and wrote its generated configs and compose file to the same generated-configs/sim directory. Container names are global in Docker and are not scoped by the compose project, so concurrent scenarios collided on both, and a scenario could start containers from a compose file another had overwritten.

Thread the existing FIPS_CI_NAME_SUFFIX into the simulation. run_chaos narrows the run-wide suffix to the scenario, and the sim reads it once when the topology is built, applying it to the container names and to the config directory basename. The compose template renders the name through the topology accessor instead of duplicating the literal, so one expression produces every chaos container name.

The suffix is empty when the variable is unset, so a bare chaos.sh run and the hosted CI jobs render byte-identical names and paths. Verified by rendering every scenario's compose file before and after with the variable unset and diffing.
This commit is contained in:
Johnathan Corgan
2026-07-22 07:35:11 +00:00
parent 8ca2362e6c
commit 5be7c6d0cb
5 changed files with 51 additions and 7 deletions
+2 -1
View File
@@ -47,7 +47,7 @@ services:
{% for node in nodes %} {% for node in nodes %}
{{ node.node_id }}: {{ node.node_id }}:
<<: *fips-common <<: *fips-common
container_name: fips-node-{{ node.node_id }} container_name: {{ topology.container_name(node.node_id) }}
hostname: {{ node.node_id }} hostname: {{ node.node_id }}
volumes: volumes:
- ./{{ node.node_id }}.yaml:/etc/fips/fips.yaml:ro - ./{{ node.node_id }}.yaml:/etc/fips/fips.yaml:ro
@@ -82,6 +82,7 @@ def generate_compose(
image=FIPS_SIM_IMAGE, image=FIPS_SIM_IMAGE,
nodes=nodes, nodes=nodes,
resolv_conf=resolv_conf, resolv_conf=resolv_conf,
topology=topology,
) )
path = os.path.join(output_dir, "docker-compose.yml") path = os.path.join(output_dir, "docker-compose.yml")
+17
View File
@@ -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", "")
+9 -1
View File
@@ -133,9 +133,17 @@ class SimRunner:
log.info(" %s: peers=%s", nid, ",".join(peers)) log.info(" %s: peers=%s", nid, ",".join(peers))
# 2. Generate configs # 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__), "..") docker_network_dir = os.path.join(os.path.dirname(__file__), "..")
config_dir = os.path.normpath( 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) # Select ephemeral identity nodes (if peer churn enabled)
self._ephemeral_nodes: set[str] = set() self._ephemeral_nodes: set[str] = set()
+13 -2
View File
@@ -8,6 +8,7 @@ from collections import deque
from dataclasses import dataclass, field from dataclasses import dataclass, field
from .keys import derive from .keys import derive
from .naming import name_suffix
from .scenario import TopologyConfig from .scenario import TopologyConfig
@@ -28,6 +29,9 @@ class SimTopology:
edges: set[tuple[str, str]] = field(default_factory=set) edges: set[tuple[str, str]] = field(default_factory=set)
# Per-edge transport type; edges not in this dict default to "udp" # Per-edge transport type; edges not in this dict default to "udp"
edge_transport: dict[tuple[str, str], str] = field(default_factory=dict) 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: def transport_for_edge(self, a: str, b: str) -> str:
"""Get the transport type for an edge (defaults to 'udp').""" """Get the transport type for an edge (defaults to 'udp')."""
@@ -107,7 +111,7 @@ class SimTopology:
return not connected return not connected
def container_name(self, node_id: str) -> str: 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]]: def directed_outbound(self) -> dict[str, list[str]]:
"""Assign each static-config edge to exactly one node for outbound connection. """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[a].peers.append(b)
nodes[b].peers.append(a) 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 # Connectivity check with retry
if config.ensure_connected: if config.ensure_connected:
+10 -3
View File
@@ -516,9 +516,16 @@ run_chaos() {
local name="$1" local name="$1"
shift shift
local rc=0 local rc=0
# Distinct project per scenario (chaos children run in parallel). When # Distinct project per scenario (chaos children run in parallel). Scoped to
# invoked from a background subshell this export is local to that child. # this function so the --only path cannot leak it into a later suite.
export COMPOSE_PROJECT_NAME="$(ci_project "chaos-$name")" 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" info "[chaos/$name] Running simulation"
if bash testing/chaos/scripts/chaos.sh "$@" 2>&1; then if bash testing/chaos/scripts/chaos.sh "$@" 2>&1; then