From f8c5c55d11ea35e231ddca41aa8b79ad51b931cd Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 22 Jul 2026 00:49:43 +0100 Subject: [PATCH] Let --report-ip lead host identity and pass it through the installer An explicit --report-ip is the user naming the primary address on a multi-NIC host, but identityFromHost appended it after the auto-detected interface addresses while every consumer of ResourceIdentity.IPAddresses treats the first entry as primary, so the override never changed what the Machines table displayed. Prepend it instead. The install script also rejected --report-ip as an unknown argument even though the agent supports the flag, forcing hand edits to the service unit that a later --update run would drop. Accept the flag, render it into the service ExecStart, persist it in connection state, and recognise it during saved-state and arg-stream recovery so updates preserve it. Refs #829 Contract-Neutral: behavioral fix: user-specified report-ip leads host identity addresses and the installer passes --report-ip through; no public contract change (#829) --- internal/unifiedresources/adapters.go | 5 ++++- internal/unifiedresources/adapters_test.go | 16 ++++++++++++++ scripts/install.sh | 21 +++++++++++++++++-- .../agent_state_dir_lifecycle_test.go | 2 ++ scripts/installtests/install_sh_test.go | 7 ++++++- 5 files changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/unifiedresources/adapters.go b/internal/unifiedresources/adapters.go index 86edfc1363..2f540df3a7 100644 --- a/internal/unifiedresources/adapters.go +++ b/internal/unifiedresources/adapters.go @@ -93,7 +93,10 @@ func resourceFromProxmoxNode(node models.Node, linkedHost *models.Host) (Resourc func identityFromHost(host models.Host) ResourceIdentity { ips, macs := collectInterfaceIDs(host.NetworkInterfaces) if host.ReportIP != "" { - ips = append(ips, host.ReportIP) + // --report-ip is the user naming the primary address on a multi-NIC + // host, and consumers treat the first entry as primary, so it must + // lead the list rather than trail the auto-detected ones (#829). + ips = append([]string{host.ReportIP}, ips...) } return ResourceIdentity{ diff --git a/internal/unifiedresources/adapters_test.go b/internal/unifiedresources/adapters_test.go index c15e732791..555bc82024 100644 --- a/internal/unifiedresources/adapters_test.go +++ b/internal/unifiedresources/adapters_test.go @@ -1752,6 +1752,22 @@ func TestCollectInterfaceIDsOrdersPhysicalInterfacesFirst(t *testing.T) { } } +func TestIdentityFromHostReportIPLeadsAddresses(t *testing.T) { + host := models.Host{ + Hostname: "multi-nic", + ReportIP: "192.168.1.9", + NetworkInterfaces: []models.HostNetworkInterface{ + {Name: "eth0", Addresses: []string{"2.2.2.10/24"}}, + {Name: "eth1", Addresses: []string{"192.168.1.9/24"}}, + }, + } + + identity := identityFromHost(host) + if len(identity.IPAddresses) == 0 || identity.IPAddresses[0] != "192.168.1.9" { + t.Fatalf("expected --report-ip to lead identity addresses, got %v", identity.IPAddresses) + } +} + func TestCollectInterfaceIDsAllVirtualStillCollects(t *testing.T) { interfaces := []models.HostNetworkInterface{ {Name: "docker0", Addresses: []string{"172.17.0.1/16"}}, diff --git a/scripts/install.sh b/scripts/install.sh index 319bd6c204..3b09be39b1 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -83,6 +83,7 @@ SERVER_FINGERPRINT="${PULSE_SERVER_FINGERPRINT:-}" OBSERVERS_FILE="${PULSE_OBSERVERS_FILE:-}" AGENT_ID="" HOSTNAME_OVERRIDE="" +REPORT_IP="" ENABLE_COMMANDS="false" HEALTH_ADDR="${PULSE_HEALTH_ADDR:-}" HEALTH_ADDR_SET="false" @@ -331,6 +332,7 @@ Options: --enable-proxmox Force enable Proxmox integration --agent-id Custom agent identifier --hostname Override hostname reported to Pulse + --report-ip IP address to report to Pulse (for multi-NIC systems) --state-dir Override persistent state directory --disk-exclude Exclude mount point (repeatable) --insecure Skip TLS verification (auto-enabled for http:// URLs) @@ -1530,6 +1532,7 @@ build_exec_arg_items() { if [[ "$KUBE_INCLUDE_ALL_DEPLOYMENTS" == "true" ]]; then EXEC_ARG_ITEMS+=(--kube-include-all-deployments); fi if [[ -n "$AGENT_ID" ]]; then EXEC_ARG_ITEMS+=(--agent-id "$AGENT_ID"); fi if [[ -n "$HOSTNAME_OVERRIDE" ]]; then EXEC_ARG_ITEMS+=(--hostname "$HOSTNAME_OVERRIDE"); fi + if [[ -n "$REPORT_IP" ]]; then EXEC_ARG_ITEMS+=(--report-ip "$REPORT_IP"); fi if [[ -n "$STATE_DIR" ]]; then EXEC_ARG_ITEMS+=(--state-dir "$STATE_DIR"); fi # Add disk exclude patterns (use ${arr[@]+"${arr[@]}"} for bash 3.2 compatibility with set -u) for pattern in ${DISK_EXCLUDES[@]+"${DISK_EXCLUDES[@]}"}; do @@ -1749,6 +1752,9 @@ recover_connection_state() { if [[ -z "$HOSTNAME_OVERRIDE" ]]; then HOSTNAME_OVERRIDE=$(read_connection_state_value "$file" "PULSE_HOSTNAME") fi + if [[ -z "$REPORT_IP" ]]; then + REPORT_IP=$(read_connection_state_value "$file" "PULSE_REPORT_IP") + fi if [[ "$INSECURE" != "true" ]]; then local saved_insecure="" saved_insecure=$(read_connection_state_value "$file" "PULSE_INSECURE_SKIP_VERIFY") @@ -1817,6 +1823,10 @@ apply_recovered_agent_arg_value() { if [[ -z "$HOSTNAME_OVERRIDE" ]]; then HOSTNAME_OVERRIDE="$value"; fi RECOVERED_AGENT_ARG_STATE="true" ;; + report-ip) + if [[ -z "$REPORT_IP" ]]; then REPORT_IP="$value"; fi + RECOVERED_AGENT_ARG_STATE="true" + ;; cacert) if [[ -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE="$value"; fi RECOVERED_AGENT_ARG_STATE="true" @@ -1888,10 +1898,10 @@ recover_connection_state_from_arg_stream() { fi case "$arg" in - --url|--pulse-url|--token|--token-file|--interval|--agent-id|--hostname|--cacert|--server-fingerprint|--observers-file|--health-addr|--state-dir|--kubeconfig|--proxmox-type|--disk-exclude|-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-server-fingerprint|-observers-file|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude) + --url|--pulse-url|--token|--token-file|--interval|--agent-id|--hostname|--report-ip|--cacert|--server-fingerprint|--observers-file|--health-addr|--state-dir|--kubeconfig|--proxmox-type|--disk-exclude|-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-report-ip|-cacert|-server-fingerprint|-observers-file|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude) pending_key=$(normalize_recovered_agent_arg_key "$arg") ;; - --url=*|--pulse-url=*|--token=*|--token-file=*|--interval=*|--agent-id=*|--hostname=*|--cacert=*|--server-fingerprint=*|--observers-file=*|--health-addr=*|--state-dir=*|--kubeconfig=*|--proxmox-type=*|--disk-exclude=*|-url=*|-pulse-url=*|-token=*|-token-file=*|-interval=*|-agent-id=*|-hostname=*|-cacert=*|-server-fingerprint=*|-observers-file=*|-health-addr=*|-state-dir=*|-kubeconfig=*|-proxmox-type=*|-disk-exclude=*) + --url=*|--pulse-url=*|--token=*|--token-file=*|--interval=*|--agent-id=*|--hostname=*|--report-ip=*|--cacert=*|--server-fingerprint=*|--observers-file=*|--health-addr=*|--state-dir=*|--kubeconfig=*|--proxmox-type=*|--disk-exclude=*|-url=*|-pulse-url=*|-token=*|-token-file=*|-interval=*|-agent-id=*|-hostname=*|-report-ip=*|-cacert=*|-server-fingerprint=*|-observers-file=*|-health-addr=*|-state-dir=*|-kubeconfig=*|-proxmox-type=*|-disk-exclude=*) key="${arg%%=*}" value="${arg#*=}" apply_recovered_agent_arg_value "$key" "$value" @@ -2021,6 +2031,11 @@ recover_connection_state_from_env_stream() { if [[ -z "$HOSTNAME_OVERRIDE" ]]; then HOSTNAME_OVERRIDE="$value"; fi RECOVERED_AGENT_ENV_STATE="true" ;; + PULSE_REPORT_IP=*) + value="${env_line#*=}" + if [[ -z "$REPORT_IP" ]]; then REPORT_IP="$value"; fi + RECOVERED_AGENT_ENV_STATE="true" + ;; PULSE_INSECURE_SKIP_VERIFY=true) INSECURE="true" RECOVERED_AGENT_ENV_STATE="true" @@ -2409,6 +2424,7 @@ save_connection_info() { write_connection_state_value "$conn_tmp" "PULSE_TOKEN_FILE" "$RUNTIME_TOKEN_FILE" write_connection_state_value "$conn_tmp" "PULSE_AGENT_ID" "$AGENT_ID" write_connection_state_value "$conn_tmp" "PULSE_HOSTNAME" "$HOSTNAME_OVERRIDE" + write_connection_state_value "$conn_tmp" "PULSE_REPORT_IP" "$REPORT_IP" if [[ "$INSECURE" == "true" ]]; then write_connection_state_value "$conn_tmp" "PULSE_INSECURE_SKIP_VERIFY" "true" fi @@ -2477,6 +2493,7 @@ while [[ $# -gt 0 ]]; do --uninstall) UNINSTALL="true"; shift ;; --agent-id) AGENT_ID="$2"; shift 2 ;; --hostname) HOSTNAME_OVERRIDE="$2"; shift 2 ;; + --report-ip) REPORT_IP="$2"; shift 2 ;; --state-dir) STATE_DIR="$2"; STATE_DIR_SOURCE="explicit"; shift 2 ;; --kube-include-all-pods) KUBE_INCLUDE_ALL_PODS="true"; shift ;; --kube-include-all-deployments) KUBE_INCLUDE_ALL_DEPLOYMENTS="true"; shift ;; diff --git a/scripts/installtests/agent_state_dir_lifecycle_test.go b/scripts/installtests/agent_state_dir_lifecycle_test.go index b0782cbb0a..20da721536 100644 --- a/scripts/installtests/agent_state_dir_lifecycle_test.go +++ b/scripts/installtests/agent_state_dir_lifecycle_test.go @@ -178,6 +178,7 @@ func renderLifecycleService(t *testing.T, stateDir, stateSource, unitPath, pulse PULSE_TOKEN="" AGENT_ID="" HOSTNAME_OVERRIDE="" + REPORT_IP="" INSECURE="false" SERVER_FINGERPRINT="" CURL_CA_BUNDLE="" @@ -211,6 +212,7 @@ func renderLifecycleService(t *testing.T, stateDir, stateSource, unitPath, pulse KUBE_INCLUDE_ALL_DEPLOYMENTS="false" AGENT_ID="" HOSTNAME_OVERRIDE="state-lifecycle-host" + REPORT_IP="" DISK_EXCLUDES=() CURL_CA_BUNDLE="" RUNTIME_TOKEN_FILE="" diff --git a/scripts/installtests/install_sh_test.go b/scripts/installtests/install_sh_test.go index 03af150aaf..1e532db882 100644 --- a/scripts/installtests/install_sh_test.go +++ b/scripts/installtests/install_sh_test.go @@ -716,7 +716,7 @@ func TestInstallSHSupportsSavedStateUpdateMode(t *testing.T) { `recover_connection_state_from_arg_stream`, `recover_token_from_default_agent_token_file() {`, `normalize_recovered_agent_arg_key() {`, - `-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-cacert|-server-fingerprint|-observers-file|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude)`, + `-url|-pulse-url|-token|-token-file|-interval|-agent-id|-hostname|-report-ip|-cacert|-server-fingerprint|-observers-file|-health-addr|-state-dir|-kubeconfig|-proxmox-type|-disk-exclude)`, `--enable-host|-enable-host|--enable-host=true|-enable-host=true)`, `recover_connection_state_from_env_stream`, `recovered_connection_state_ready() {`, @@ -2582,6 +2582,7 @@ func TestInstallSHConnectionEnvPersistsCanonicalStateDirWithoutTokenValue(t *tes RUNTIME_TOKEN_FILE="$STATE_DIR/token" AGENT_ID="agent-custom" HOSTNAME_OVERRIDE="host-custom" + REPORT_IP="192.168.1.9" INSECURE="false" SERVER_FINGERPRINT="" CURL_CA_BUNDLE="" @@ -2601,6 +2602,9 @@ func TestInstallSHConnectionEnvPersistsCanonicalStateDirWithoutTokenValue(t *tes !strings.Contains(got, "PULSE_TOKEN_FILE='"+filepath.Join(stateDir, "token")+"'") { t.Fatalf("connection.env did not persist canonical state paths:\n%s", got) } + if !strings.Contains(got, "PULSE_REPORT_IP='192.168.1.9'") { + t.Fatalf("connection.env did not persist the report IP:\n%s", got) + } if strings.Contains(got, "PULSE_TOKEN='") || strings.Contains(got, "deadbeef") { t.Fatalf("connection.env leaked the token value:\n%s", got) } @@ -2643,6 +2647,7 @@ func TestInstallSHStateWritesReplaceSymlinksAtomically(t *testing.T) { ENROLL="false" AGENT_ID="agent-one" HOSTNAME_OVERRIDE="host-one" + REPORT_IP="" INSECURE="false" SERVER_FINGERPRINT="" CURL_CA_BUNDLE=""