diff --git a/README.md b/README.md index b844491..676191f 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,22 @@ StatefulSet pod address is stable. The module writes all supplied roots into one non-advertised local-root group and sets its valency to the number of access points. Omitting `topology.local_roots` preserves the prior instance shape. + +Peers outside the fleet, such as another operator's relay, go in +`topology.external_local_root_groups`. Each entry renders as its own +non-advertised local-root group after the fleet group, with a valency equal to +its number of access points, so it can be added or removed without touching the +fleet mesh. External groups do not create a `nodes-` Service. + +```hcl +topology = { + local_roots = [ /* fleet peers */ ] + external_local_root_groups = [ + { + access_points = [ + { address = "relay.peer-operator.example", port = 3001 }, + ] + }, + ] +} +``` diff --git a/bootstrap/configs/main.tf b/bootstrap/configs/main.tf index bc049c9..c59c1cf 100644 --- a/bootstrap/configs/main.tf +++ b/bootstrap/configs/main.tf @@ -27,27 +27,43 @@ variable "local_roots" { default = [] } +variable "external_local_root_groups" { + description = "local-root groups for peers outside the fleet, each rendered as its own non-advertised group after local_roots" + type = list(object({ + access_points = list(object({ + address = string + port = number + })) + })) + default = [] +} + locals { baseline_topology_json = file("${path.module}/${var.network}/topology.json") baseline_topology = jsondecode(local.baseline_topology_json) + local_root_groups = [ + for group in concat( + length(var.local_roots) == 0 ? [] : [var.local_roots], + [for group in var.external_local_root_groups : group.access_points], + ) : { + accessPoints = [ + for root in group : { + address = root.address + port = root.port + } + ] + advertise = false + trustable = false + valency = length(group) + } + ] + rendered_topology = merge(local.baseline_topology, { - localRoots = length(var.local_roots) == 0 ? local.baseline_topology.localRoots : [ - { - accessPoints = [ - for root in var.local_roots : { - address = root.address - port = root.port - } - ] - advertise = false - trustable = false - valency = length(var.local_roots) - } - ] + localRoots = length(local.local_root_groups) == 0 ? local.baseline_topology.localRoots : local.local_root_groups }) - topology_json = length(var.local_roots) == 0 ? local.baseline_topology_json : jsonencode(local.rendered_topology) + topology_json = length(local.local_root_groups) == 0 ? local.baseline_topology_json : jsonencode(local.rendered_topology) } resource "kubernetes_config_map" "node-config" { diff --git a/bootstrap/configs/mainnet/README.md b/bootstrap/configs/mainnet/README.md index becbd3d..5253437 100644 --- a/bootstrap/configs/mainnet/README.md +++ b/bootstrap/configs/mainnet/README.md @@ -5,8 +5,9 @@ custom P2P topology. They preserve the node's bootstrap peers, ledger-peer activation, peer snapshot, tracing, and protocol configuration. `topology.json` supplies the P2P baseline. The module adds an instance's -configured local roots to it as one non-advertised root group, with a valency -equal to the number of access points. +configured local roots to it as one non-advertised root group, followed by one +non-advertised group per external local-root group, each with a valency equal +to its number of access points. The genesis, checkpoint, and peer-snapshot paths are absolute because the module mounts only `config.json` and `topology.json` at `/configuration`. The diff --git a/bootstrap/main.tf b/bootstrap/main.tf index a528b4b..4e3f3fe 100644 --- a/bootstrap/main.tf +++ b/bootstrap/main.tf @@ -86,7 +86,7 @@ module "instances" { availability_sla = coalesce(each.value.availability_sla, "consistent") node_version = each.value.node_version restore = coalesce(each.value.restore, false) - is_custom = coalesce(each.value.is_custom, false) || length(each.value.topology.local_roots) > 0 + is_custom = coalesce(each.value.is_custom, false) || length(each.value.topology.local_roots) > 0 || length(each.value.topology.external_local_root_groups) > 0 has_local_roots = length(each.value.topology.local_roots) > 0 is_relay = coalesce(each.value.is_relay, false) tolerations = coalesce(each.value.tolerations, []) @@ -105,13 +105,14 @@ module "custom_configs" { source = "./configs" for_each = { for key, instance in var.instances : key => instance - if coalesce(instance.is_custom, false) || length(instance.topology.local_roots) > 0 + if coalesce(instance.is_custom, false) || length(instance.topology.local_roots) > 0 || length(instance.topology.external_local_root_groups) > 0 } - namespace = var.namespace - network = each.value.network - salt = each.value.salt - local_roots = each.value.topology.local_roots + namespace = var.namespace + network = each.value.network + salt = each.value.salt + local_roots = each.value.topology.local_roots + external_local_root_groups = each.value.topology.external_local_root_groups } module "services" { diff --git a/bootstrap/tests/mempool_mesh.tftest.hcl b/bootstrap/tests/mempool_mesh.tftest.hcl index 0a5fd34..28bbd30 100644 --- a/bootstrap/tests/mempool_mesh.tftest.hcl +++ b/bootstrap/tests/mempool_mesh.tftest.hcl @@ -205,3 +205,113 @@ run "custom_instance_without_local_roots_preserves_baseline" { error_message = "a custom instance without local roots must preserve its baseline topology bytes" } } + +run "external_local_root_group_is_separate" { + command = plan + + variables { + namespace = "test-namespace" + operator_image_tag = "test" + api_key_salt = "test" + proxy_blue_image_tag = "test" + proxy_blue_instances_namespace = "test-namespace" + proxy_blue_healthcheck_port = 31789 + proxy_green_image_tag = "test" + proxy_green_instances_namespace = "test-namespace" + proxy_green_healthcheck_port = 32171 + services = {} + + instances = { + mainnet-mesh-a = { + node_image = "ghcr.io/blinklabs-io/cardano-node" + image_tag = "11.0.1" + network = "mainnet" + salt = "a" + release = "mesh" + magic = 764824073 + node_version = "11.0.1" + replicas = 1 + topology = { + local_roots = [ + { address = "node-mainnet-b-0.nodes-b.test-namespace.svc.cluster.local", port = 3000 }, + { address = "node-mainnet-c-0.nodes-c.test-namespace.svc.cluster.local", port = 3000 }, + { address = "node-mainnet-d-0.nodes-d.test-namespace.svc.cluster.local", port = 3000 }, + ] + external_local_root_groups = [ + { access_points = [{ address = "relay.peer-operator.example", port = 3001 }] }, + ] + } + } + } + } + + assert { + condition = length(module.custom_configs["mainnet-mesh-a"].topology.localRoots) == 2 + error_message = "an external group must render as its own local-root group" + } + + assert { + condition = ( + module.custom_configs["mainnet-mesh-a"].topology.localRoots[0].valency == 3 && + length(module.custom_configs["mainnet-mesh-a"].topology.localRoots[0].accessPoints) == 3 + ) + error_message = "an external group must not change the fleet local-root group" + } + + assert { + condition = ( + module.custom_configs["mainnet-mesh-a"].topology.localRoots[1].advertise == false && + module.custom_configs["mainnet-mesh-a"].topology.localRoots[1].trustable == false && + module.custom_configs["mainnet-mesh-a"].topology.localRoots[1].valency == 1 && + length(module.custom_configs["mainnet-mesh-a"].topology.localRoots[1].accessPoints) == 1 && + module.custom_configs["mainnet-mesh-a"].topology.localRoots[1].accessPoints[0].address == "relay.peer-operator.example" && + module.custom_configs["mainnet-mesh-a"].topology.localRoots[1].accessPoints[0].port == 3001 + ) + error_message = "an external group must be non-advertised, untrusted, and hot for each of its access points" + } +} + +run "external_local_root_group_without_fleet_roots" { + command = plan + + variables { + namespace = "test-namespace" + operator_image_tag = "test" + api_key_salt = "test" + proxy_blue_image_tag = "test" + proxy_blue_instances_namespace = "test-namespace" + proxy_blue_healthcheck_port = 31789 + proxy_green_image_tag = "test" + proxy_green_instances_namespace = "test-namespace" + proxy_green_healthcheck_port = 32171 + services = {} + + instances = { + mainnet-external-a = { + node_image = "ghcr.io/blinklabs-io/cardano-node" + image_tag = "11.0.1" + network = "mainnet" + salt = "a" + release = "external" + magic = 764824073 + node_version = "11.0.1" + replicas = 1 + topology = { + external_local_root_groups = [ + { access_points = [{ address = "relay.peer-operator.example", port = 3001 }] }, + ] + } + } + } + } + + assert { + condition = length(module.custom_configs["mainnet-external-a"].topology.localRoots) == 1 + error_message = "an instance with only an external group must render exactly that group" + } + + assert { + condition = module.instances["mainnet-external-a"].peer_service == null + error_message = "an external group alone must not create a headless peer Service" + } +} diff --git a/bootstrap/variables.tf b/bootstrap/variables.tf index 639e19f..cc10f95 100644 --- a/bootstrap/variables.tf +++ b/bootstrap/variables.tf @@ -231,6 +231,12 @@ variable "instances" { address = string port = number })), []) + external_local_root_groups = optional(list(object({ + access_points = list(object({ + address = string + port = number + })) + })), []) }), {}) rts_opts = optional(string) readiness_probe = optional(object({