diff --git a/.kiro/specs/controller-peers-env-fallback/tasks.md b/.kiro/specs/controller-peers-env-fallback/tasks.md index 179725c9..805ee20f 100644 --- a/.kiro/specs/controller-peers-env-fallback/tasks.md +++ b/.kiro/specs/controller-peers-env-fallback/tasks.md @@ -42,7 +42,7 @@ - Done when, after rebuilding the controller, the controller suite passes and every controller case in the regression script passes - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.7, 1.8_ -- [ ] 2.4 Refuse an env-supplied peers list that does not contain the controller's own name +- [x] 2.4 Refuse an env-supplied peers list that does not contain the controller's own name - Add the cases to the regression script first and see them fail: with a three-candidate variable, a name not in it exits non-zero with the message; a name in it logs "HA mode: 3 controller candidates" with no such message; the same list given as explicit `--peers` with a name not in it prints no such message - When the list came from the variable and the shared lookup finds no candidate for the controller's own name (its `--myname`, or the host name when not given), print to stderr the controller's name, the variable as the source, the candidates (by name, or hostname when unnamed) and the two remedies (pass `--myname` naming a candidate, or unset the variable); flush the log and exit non-zero before the certificate pre-flight and any network activity - Leave the explicit-`--peers` path untouched (its later refusal in the listener stays as it is) diff --git a/src/controller/main.c b/src/controller/main.c index e88187b3..b3741d91 100644 --- a/src/controller/main.c +++ b/src/controller/main.c @@ -495,6 +495,36 @@ int main(int argc, char* argv[]) return 1; } + /* Req 1.6: a list taken from the environment must name this + * controller, or a stray variable would silently join it to a + * cluster it is not part of. Refuse before the cert pre-flight and + * any network activity. An explicit --peers keeps its existing + * handling (the later refusal in cluster_open_listener()). */ + if (peers_from_env && + xce_peers_find_identity(&myconf.peers_cfg, arguments.myname) < 0) { + char names[512]; + size_t used = 0; + names[0] = '\0'; + for (int i = 0; i < myconf.peers_cfg.candidates_count; i++) { + const xce_candidate_t* c = &myconf.peers_cfg.candidates[i]; + int n = snprintf(names + used, sizeof(names) - used, "%s%s", + i > 0 ? ", " : "", + c->name[0] != '\0' ? c->name : c->hostname); + if (n < 0 || (size_t)n >= sizeof(names) - used) { + names[used] = '\0'; /* drop the partial last name */ + break; + } + used += (size_t)n; + } + fprintf(stderr, + "xce_controller: XCE_CONTROLLER_PEERS (%s) has no " + "candidate named '%s'; pass --myname or " + "unset XCE_CONTROLLER_PEERS to run standalone\n", + peers_str, arguments.myname, names); + mylog_fini(); + return 1; + } + strncpy(myconf.peers_cfg.identity, arguments.myname, sizeof(myconf.peers_cfg.identity) - 1); INFO("HA mode: %d controller candidates, identity=%s%s", diff --git a/src/tools/verify-controller-peers-env.sh b/src/tools/verify-controller-peers-env.sh index abeee2f6..0b10eced 100755 --- a/src/tools/verify-controller-peers-env.sh +++ b/src/tools/verify-controller-peers-env.sh @@ -456,6 +456,54 @@ case_controller_help_names_variable() { add_case case_controller_help_names_variable \ "controller: --help documents XCE_CONTROLLER_PEERS and the opt-out (1.5, 1.7)" +# ── Task 2.4 (Req 1.6): an XCE_CONTROLLER_PEERS list that has no candidate for +# the controller's own name is refused on stderr before the certificate +# pre-flight. Without the check these runs still exit non-zero at the +# pre-flight, so the cases assert on the message, not just the exit status. +OWN_NAME_RE="has no candidate named" + +case_controller_env_own_name_missing() { + local d; d="$(new_scratch controller-own-missing)" + run_controller "$d" "XCE_CONTROLLER_PEERS=$ENV3" -- --myname zz + assert_rc nonzero || return 1 + assert_grep "$d/stderr" "XCE_CONTROLLER_PEERS \\($ENV3\\) $OWN_NAME_RE 'zz'" "stderr" || return 1 + assert_grep "$d/stderr" -- '--myname ' "stderr" || return 1 + assert_grep "$d/stderr" 'unset XCE_CONTROLLER_PEERS' "stderr" || return 1 + assert_no_grep "$d/stderr" 'cert|key' "stderr (must stop before the certificate pre-flight)" || return 1 +} +add_case case_controller_env_own_name_missing \ + "controller: env list without the own name is refused on stderr (1.6)" + +case_controller_env_own_name_unnamed() { + local d; d="$(new_scratch controller-own-unnamed)" + run_controller "$d" "XCE_CONTROLLER_PEERS=localhost:1" -- --myname zz + assert_rc nonzero || return 1 + assert_grep "$d/stderr" "$OWN_NAME_RE 'zz'" "stderr" || return 1 + assert_grep "$d/stderr" -- '--myname ' "stderr (unnamed candidate listed by hostname)" || return 1 +} +add_case case_controller_env_own_name_unnamed \ + "controller: unnamed env candidates are listed by hostname in the 1.6 message" + +case_controller_env_own_name_present() { + local d; d="$(new_scratch controller-own-present)" + run_controller "$d" "XCE_CONTROLLER_PEERS=$ENV3" -- --myname b + _assert_stopped_early "$d" || return 1 + assert_grep "$d/controller.log" 'HA mode: 3 controller candidates, identity=b' || return 1 + assert_no_grep "$d/stderr" "$OWN_NAME_RE" "stderr" || return 1 +} +add_case case_controller_env_own_name_present \ + "controller: env list containing the own name is accepted (1.6)" + +case_controller_flag_own_name_unchecked() { + local d; d="$(new_scratch controller-own-flag)" + run_controller "$d" -u XCE_CONTROLLER_PEERS -- --myname zz --peers "$ENV3" + _assert_stopped_early "$d" || return 1 + assert_grep "$d/controller.log" 'HA mode: 3 controller candidates, identity=zz' || return 1 + assert_no_grep "$d/stderr" "$OWN_NAME_RE" "stderr" || return 1 +} +add_case case_controller_flag_own_name_unchecked \ + "controller: explicit --peers without the own name is not checked at startup (1.6)" + # ════════════════════════════════════════════════════════════════════════════ # main