Support agent retargeting after server moves

This commit is contained in:
pulse-triage[bot]
2026-08-30 16:04:08 +01:00
parent c3faa48973
commit 323da54067
11 changed files with 321 additions and 16 deletions
+5
View File
@@ -370,6 +370,11 @@ You can auto-import an encrypted backup on first startup. This is useful for aut
> **Note**: `PULSE_INIT_CONFIG_URL` is only supported by the hidden `pulse config auto-import` command, not by the server startup auto-import.
Configuration import restores server-side agent credentials, but it cannot
change the primary Pulse URL persisted on remote agents. When restoring onto a
server with a different address, retarget those agents after import. See
[Moving Pulse to a new address](UNIFIED_AGENT.md#moving-pulse-to-a-new-address).
### Developer/Test Overrides (Environment Variables)
These are primarily for development or test harnesses and should not be used in production.
+29
View File
@@ -31,6 +31,35 @@ Run it on the host that already has the v5 `pulse-agent` service to replace the
binary and service configuration in place; do not uninstall the old service
first unless you are intentionally removing that host from Pulse.
### Moving Pulse to a new address
Configuration export/import restores the server-side agent records and API
tokens. It cannot rewrite the primary Pulse URL on remote machines because
agents initiate the connection. Prefer a stable DNS name for the primary URL
so replacing the Pulse host does not require an agent migration.
After importing the configuration on a Pulse server with a different address,
retarget each existing standard Linux agent from that agent machine:
```bash
curl -fsSL https://pulse.example.com:7655/install.sh | \
sudo bash -s -- --retarget --url https://pulse.example.com:7655
```
The retarget operation recovers the existing token, agent ID, enabled
collectors, and other service options. It does not carry the old endpoint's
TLS bypass, custom CA, or certificate fingerprint to the new address. Supply
`--cacert`, `--server-fingerprint`, or (only on a trusted network)
`--insecure` explicitly when the new endpoint requires it. The script must
come from the new server so it supports the retarget operation. A newly
generated full installation command from **Settings → Infrastructure → Install
on a host** remains the fallback.
On Windows, run the full generated PowerShell installation command from the
new Pulse server as Administrator, including the desired collector options.
Do not expect the configuration import itself to make agent-only machines
appear at the new address.
An installed agent has one **primary** Pulse URL and token. The primary is the
only server allowed to supply remote configuration, commands, enrollment, or
updates. The same collection can also be sent to explicitly configured,
@@ -370,6 +370,11 @@ You can auto-import an encrypted backup on first startup. This is useful for aut
> **Note**: `PULSE_INIT_CONFIG_URL` is only supported by the hidden `pulse config auto-import` command, not by the server startup auto-import.
Configuration import restores server-side agent credentials, but it cannot
change the primary Pulse URL persisted on remote agents. When restoring onto a
server with a different address, retarget those agents after import. See
[Moving Pulse to a new address](UNIFIED_AGENT.md#moving-pulse-to-a-new-address).
### Developer/Test Overrides (Environment Variables)
These are primarily for development or test harnesses and should not be used in production.
@@ -31,6 +31,35 @@ Run it on the host that already has the v5 `pulse-agent` service to replace the
binary and service configuration in place; do not uninstall the old service
first unless you are intentionally removing that host from Pulse.
### Moving Pulse to a new address
Configuration export/import restores the server-side agent records and API
tokens. It cannot rewrite the primary Pulse URL on remote machines because
agents initiate the connection. Prefer a stable DNS name for the primary URL
so replacing the Pulse host does not require an agent migration.
After importing the configuration on a Pulse server with a different address,
retarget each existing standard Linux agent from that agent machine:
```bash
curl -fsSL https://pulse.example.com:7655/install.sh | \
sudo bash -s -- --retarget --url https://pulse.example.com:7655
```
The retarget operation recovers the existing token, agent ID, enabled
collectors, and other service options. It does not carry the old endpoint's
TLS bypass, custom CA, or certificate fingerprint to the new address. Supply
`--cacert`, `--server-fingerprint`, or (only on a trusted network)
`--insecure` explicitly when the new endpoint requires it. The script must
come from the new server so it supports the retarget operation. A newly
generated full installation command from **Settings → Infrastructure → Install
on a host** remains the fallback.
On Windows, run the full generated PowerShell installation command from the
new Pulse server as Administrator, including the desired collector options.
Do not expect the configuration import itself to make agent-only machines
appear at the new address.
An installed agent has one **primary** Pulse URL and token. The primary is the
only server allowed to supply remote configuration, commands, enrollment, or
updates. The same collection can also be sent to explicitly configured,
+5 -1
View File
@@ -10,9 +10,13 @@ const targetDocsDir = path.join(frontendRoot, 'public', 'docs');
const shippedDocs = [
{ source: path.join(repoRoot, 'docs', 'README.md'), target: 'README.md' },
{ source: path.join(repoRoot, 'docs', 'MIGRATION_UNIFIED_NAV.md'), target: 'MIGRATION_UNIFIED_NAV.md' },
{
source: path.join(repoRoot, 'docs', 'MIGRATION_UNIFIED_NAV.md'),
target: 'MIGRATION_UNIFIED_NAV.md',
},
{ source: path.join(repoRoot, 'docs', 'PRIVACY.md'), target: 'PRIVACY.md' },
{ source: path.join(repoRoot, 'docs', 'CONFIGURATION.md'), target: 'CONFIGURATION.md' },
{ source: path.join(repoRoot, 'docs', 'UNIFIED_AGENT.md'), target: 'UNIFIED_AGENT.md' },
{ source: path.join(repoRoot, 'docs', 'PROXY_AUTH.md'), target: 'PROXY_AUTH.md' },
{ source: path.join(repoRoot, 'docs', 'TRUENAS.md'), target: 'TRUENAS.md' },
{ source: path.join(repoRoot, 'SECURITY.md'), target: 'SECURITY.md' },
@@ -270,10 +270,17 @@ export const BackupTransferDialogs: Component<BackupTransferDialogsProps> = (pro
</div>
<div class="bg-yellow-50 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-800 rounded p-3">
<p class="text-xs text-yellow-700 dark:text-yellow-300">
<strong>Warning:</strong> Importing will replace all current configuration. This
action cannot be undone.
</p>
<div class="space-y-2 text-xs text-yellow-700 dark:text-yellow-300">
<p>
<strong>Warning:</strong> Importing will replace all current configuration. This
action cannot be undone.
</p>
<p>
<strong>Agent migration:</strong> The backup restores server-side agent records
and credentials, but it cannot change the Pulse URL stored on remote agents. If
this server has a different address, retarget each agent after the import.
</p>
</div>
</div>
<div class="flex justify-end space-x-3">
@@ -0,0 +1,41 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { BackupTransferDialogs } from '../BackupTransferDialogs';
describe('BackupTransferDialogs', () => {
afterEach(() => {
cleanup();
});
it('discloses that configuration import cannot retarget remote agents', () => {
render(() => (
<BackupTransferDialogs
securityStatus={() => null}
exportPassphrase={() => ''}
setExportPassphrase={vi.fn()}
useCustomPassphrase={() => false}
setUseCustomPassphrase={vi.fn()}
importPassphrase={() => 'secure-passphrase'}
setImportPassphrase={vi.fn()}
importFile={() => null}
setImportFile={vi.fn()}
showExportDialog={() => false}
showImportDialog={() => true}
showApiTokenModal={() => false}
apiTokenInput={() => ''}
setApiTokenInput={vi.fn()}
handleExport={vi.fn()}
handleImport={vi.fn()}
closeExportDialog={vi.fn()}
closeImportDialog={vi.fn()}
closeApiTokenModal={vi.fn()}
handleApiTokenAuthenticate={vi.fn()}
/>
));
expect(screen.getByText('Agent migration:')).toBeInTheDocument();
expect(
screen.getByText(/it cannot change the Pulse URL stored on remote agents/i),
).toBeInTheDocument();
});
});
@@ -207,5 +207,9 @@ func (h *ConfigHandlers) handleImportConfig(w http.ResponseWriter, r *http.Reque
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "success",
"message": "Configuration imported successfully",
"remoteAgentEndpoints": map[string]bool{
"reconfigured": false,
"actionRequiredIfServerAddressChanged": true,
},
})
}
@@ -174,6 +174,16 @@ func TestHandleImportConfig(t *testing.T) {
if resp["status"] != "success" {
t.Errorf("expected status success, got %v", resp["status"])
}
remoteEndpoints, ok := resp["remoteAgentEndpoints"].(map[string]interface{})
if !ok {
t.Fatalf("expected remote agent endpoint import disclosure, got %T", resp["remoteAgentEndpoints"])
}
if remoteEndpoints["reconfigured"] != false {
t.Errorf("configuration import must not claim to reconfigure remote agents: %v", remoteEndpoints)
}
if remoteEndpoints["actionRequiredIfServerAddressChanged"] != true {
t.Errorf("expected changed-address action disclosure, got %v", remoteEndpoints)
}
},
},
{
+26 -11
View File
@@ -97,8 +97,10 @@ ENABLE_KUBERNETES="" # Empty means "auto-detect"
ENABLE_PROXMOX="" # Empty means "auto-detect"
PROXMOX_TYPE=""
UPDATE_ONLY="false"
RETARGET_ONLY="false"
UNINSTALL="false"
INSECURE="false"
INSECURE_EXPLICIT="false"
SERVER_FINGERPRINT="${PULSE_SERVER_FINGERPRINT:-}"
OBSERVERS_FILE="${PULSE_OBSERVERS_FILE:-}"
AGENT_ID=""
@@ -125,7 +127,7 @@ AGENT_LOG_FILE="" # When set, pass --log-file so the agent's rotating log writer
DEFAULT_STATE_DIR="/var/lib/pulse-agent"
STATE_DIR="$DEFAULT_STATE_DIR" # Persistent state directory (overridden per platform)
STATE_DIR_SOURCE="default" # default, explicit, recovered, or platform
CURL_CA_BUNDLE="" # Path to CA bundle for curl and agent TLS (sets SSL_CERT_FILE)
CURL_CA_BUNDLE="${PULSE_CACERT:-}" # Path to CA bundle for curl and agent TLS (sets SSL_CERT_FILE)
NON_INTERACTIVE="false"
TOKEN_FILE_PATH="" # Path to file containing the token
RUNTIME_TOKEN_FILE="" # Secure token file passed to the installed service
@@ -576,6 +578,7 @@ Options:
--safe-profile-rollback Restore the prior collector/helper snapshot from the last committed safe-profile migration
--enroll Exchange bootstrap token for runtime token (deploy wizard)
--update Update an existing agent using saved connection state
--retarget Point an existing agent at --url using saved identity and token
--uninstall Remove the agent
--non-interactive Skip TTY prompts (for automated/scripted installs)
--token-file <path> Read token from file (alternative to --token)
@@ -3325,17 +3328,17 @@ recover_connection_state() {
if [[ -z "$REPORT_IP" ]]; then
REPORT_IP=$(read_connection_state_value "$file" "PULSE_REPORT_IP")
fi
if [[ "$INSECURE" != "true" ]]; then
if [[ "${RETARGET_ONLY:-false}" != "true" && "$INSECURE" != "true" ]]; then
local saved_insecure=""
saved_insecure=$(read_connection_state_value "$file" "PULSE_INSECURE_SKIP_VERIFY")
if [[ "$saved_insecure" == "true" ]]; then
INSECURE="true"
fi
fi
if [[ -z "$SERVER_FINGERPRINT" ]]; then
if [[ "${RETARGET_ONLY:-false}" != "true" && -z "$SERVER_FINGERPRINT" ]]; then
SERVER_FINGERPRINT=$(read_connection_state_value "$file" "PULSE_SERVER_FINGERPRINT")
fi
if [[ -z "$CURL_CA_BUNDLE" ]]; then
if [[ "${RETARGET_ONLY:-false}" != "true" && -z "$CURL_CA_BUNDLE" ]]; then
CURL_CA_BUNDLE=$(read_connection_state_value "$file" "PULSE_CACERT")
fi
}
@@ -3398,11 +3401,11 @@ apply_recovered_agent_arg_value() {
RECOVERED_AGENT_ARG_STATE="true"
;;
cacert)
if [[ -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE="$value"; fi
if [[ "${RETARGET_ONLY:-false}" != "true" && -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE="$value"; fi
RECOVERED_AGENT_ARG_STATE="true"
;;
server-fingerprint)
if [[ -z "$SERVER_FINGERPRINT" ]]; then SERVER_FINGERPRINT="$value"; fi
if [[ "${RETARGET_ONLY:-false}" != "true" && -z "$SERVER_FINGERPRINT" ]]; then SERVER_FINGERPRINT="$value"; fi
RECOVERED_AGENT_ARG_STATE="true"
;;
observers-file)
@@ -3565,7 +3568,9 @@ recover_connection_state_from_arg_stream() {
RECOVERED_AGENT_ARG_STATE="true"
;;
--insecure|-insecure)
INSECURE="true"
if [[ "${RETARGET_ONLY:-false}" != "true" || "${INSECURE_EXPLICIT:-false}" == "true" ]]; then
INSECURE="true"
fi
RECOVERED_AGENT_ARG_STATE="true"
;;
--enable-commands|-enable-commands)
@@ -3645,17 +3650,19 @@ recover_connection_state_from_env_stream() {
RECOVERED_AGENT_ENV_STATE="true"
;;
PULSE_INSECURE_SKIP_VERIFY=true)
INSECURE="true"
if [[ "${RETARGET_ONLY:-false}" != "true" || "${INSECURE_EXPLICIT:-false}" == "true" ]]; then
INSECURE="true"
fi
RECOVERED_AGENT_ENV_STATE="true"
;;
PULSE_CACERT=*)
value="${env_line#*=}"
if [[ -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE="$value"; fi
if [[ "${RETARGET_ONLY:-false}" != "true" && -z "$CURL_CA_BUNDLE" ]]; then CURL_CA_BUNDLE="$value"; fi
RECOVERED_AGENT_ENV_STATE="true"
;;
PULSE_SERVER_FINGERPRINT=*)
value="${env_line#*=}"
if [[ -z "$SERVER_FINGERPRINT" ]]; then SERVER_FINGERPRINT="$value"; fi
if [[ "${RETARGET_ONLY:-false}" != "true" && -z "$SERVER_FINGERPRINT" ]]; then SERVER_FINGERPRINT="$value"; fi
RECOVERED_AGENT_ENV_STATE="true"
;;
esac
@@ -4090,7 +4097,7 @@ while [[ $# -gt 0 ]]; do
--enable-proxmox=false) ENABLE_PROXMOX="false"; PROXMOX_EXPLICIT="true"; shift ;;
--disable-proxmox) ENABLE_PROXMOX="false"; PROXMOX_EXPLICIT="true"; shift ;;
--proxmox-type) PROXMOX_TYPE="$2"; shift 2 ;;
--insecure) INSECURE="true"; shift ;;
--insecure) INSECURE="true"; INSECURE_EXPLICIT="true"; shift ;;
--cacert) CURL_CA_BUNDLE="$2"; shift 2 ;;
--server-fingerprint) SERVER_FINGERPRINT="$2"; shift 2 ;;
--observers-file) OBSERVERS_FILE="$2"; shift 2 ;;
@@ -4111,6 +4118,7 @@ while [[ $# -gt 0 ]]; do
--safe-profile-rollback) SAFE_PROFILE_ACTION="rollback"; shift ;;
--enroll) ENROLL="true"; shift ;;
--update) UPDATE_ONLY="true"; shift ;;
--retarget) RETARGET_ONLY="true"; UPDATE_ONLY="true"; shift ;;
--uninstall) UNINSTALL="true"; shift ;;
--agent-id) AGENT_ID="$2"; shift 2 ;;
--hostname) HOSTNAME_OVERRIDE="$2"; shift 2 ;;
@@ -4129,6 +4137,13 @@ while [[ $# -gt 0 ]]; do
esac
done
if [[ "$RETARGET_ONLY" == "true" && -z "$PULSE_URL" ]]; then
fail "--retarget requires the new Pulse endpoint in --url" "$EXIT_MISSING_ARGS"
fi
if [[ "$RETARGET_ONLY" == "true" && "$UNINSTALL" == "true" ]]; then
fail "--retarget cannot be combined with --uninstall" "$EXIT_MISSING_ARGS"
fi
case "$SAFE_PROFILE_ACTION" in
"") ;;
inspect)
+156
View File
@@ -889,8 +889,12 @@ func TestInstallSHSupportsSavedStateUpdateMode(t *testing.T) {
script := string(content)
required := []string{
`--update Update an existing agent using saved connection state`,
`--retarget Point an existing agent at --url using saved identity and token`,
`UPDATE_ONLY="false"`,
`RETARGET_ONLY="false"`,
`--update) UPDATE_ONLY="true"; shift ;;`,
`--retarget) RETARGET_ONLY="true"; UPDATE_ONLY="true"; shift ;;`,
`--retarget requires the new Pulse endpoint in --url`,
`if [[ "$UPDATE_ONLY" == "true" || "$UNINSTALL" == "true" ]]; then`,
`lifecycle_conn_env=$(find_connection_state_file || true)`,
`recover_connection_state "$lifecycle_conn_env"`,
@@ -927,6 +931,158 @@ func TestInstallSHSupportsSavedStateUpdateMode(t *testing.T) {
}
}
func TestInstallSHRetargetPreservesIdentityWithoutOldEndpointTrust(t *testing.T) {
stateDir := t.TempDir()
tokenPath := filepath.Join(stateDir, "token")
if err := os.WriteFile(tokenPath, []byte("deadbeef\n"), 0600); err != nil {
t.Fatalf("write token fixture: %v", err)
}
connectionPath := filepath.Join(stateDir, "connection.env")
connection := strings.Join([]string{
"PULSE_URL='https://old-pulse.example.test:7655'",
"PULSE_TOKEN_FILE='" + tokenPath + "'",
"PULSE_AGENT_ID='agent-123'",
"PULSE_HOSTNAME='pve-one'",
"PULSE_INSECURE_SKIP_VERIFY='true'",
"PULSE_SERVER_FINGERPRINT='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'",
"PULSE_CACERT='/etc/pulse/old-ca.pem'",
}, "\n") + "\n"
if err := os.WriteFile(connectionPath, []byte(connection), 0600); err != nil {
t.Fatalf("write connection fixture: %v", err)
}
script := `
PULSE_URL="https://new-pulse.example.test:7655"
PULSE_TOKEN=""
RETARGET_ONLY="true"
INSECURE="false"
SERVER_FINGERPRINT=""
CURL_CA_BUNDLE=""
AGENT_ID=""
HOSTNAME_OVERRIDE=""
REPORT_IP=""
STATE_DIR="/var/lib/pulse-agent"
STATE_DIR_SOURCE="default"
DEFAULT_STATE_DIR="/var/lib/pulse-agent"
TRUENAS_STATE_DIR="/data/pulse-agent"
` + extractInstallShellFunction(t, "read_connection_state_value") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state") + `
recover_connection_state "${PULSE_TEST_CONNECTION:?}"
printf 'URL=%s\nTOKEN=%s\nAGENT_ID=%s\nHOSTNAME=%s\nINSECURE=%s\nFINGERPRINT=%s\nCACERT=%s\n' \
"$PULSE_URL" "$PULSE_TOKEN" "$AGENT_ID" "$HOSTNAME_OVERRIDE" "$INSECURE" "$SERVER_FINGERPRINT" "$CURL_CA_BUNDLE"
`
cmd := exec.Command("bash", "-c", script)
cmd.Env = append(os.Environ(), "PULSE_TEST_CONNECTION="+connectionPath)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
for _, needle := range []string{
"URL=https://new-pulse.example.test:7655",
"TOKEN=deadbeef",
"AGENT_ID=agent-123",
"HOSTNAME=pve-one",
"INSECURE=false",
"FINGERPRINT=",
"CACERT=",
} {
if !strings.Contains(got, needle) {
t.Fatalf("retarget state missing %q:\n%s", needle, got)
}
}
if strings.Contains(got, "old-pulse") || strings.Contains(got, "old-ca") || strings.Contains(got, "aaaaaaaa") {
t.Fatalf("retarget carried old endpoint trust or URL:\n%s", got)
}
}
func TestInstallSHRetargetDoesNotRecoverLegacyServiceTrust(t *testing.T) {
script := `
PULSE_URL="https://new-pulse.example.test:7655"
PULSE_TOKEN=""
RETARGET_ONLY="true"
INSECURE="false"
INSECURE_EXPLICIT="false"
SERVER_FINGERPRINT=""
CURL_CA_BUNDLE=""
INTERVAL="30s"
INTERVAL_EXPLICIT="false"
ENABLE_HOST="true"
HOST_EXPLICIT="false"
ENABLE_DOCKER=""
DOCKER_EXPLICIT="false"
ENABLE_KUBERNETES=""
KUBERNETES_EXPLICIT="false"
KUBECONFIG_PATH=""
ENABLE_PROXMOX=""
PROXMOX_EXPLICIT="false"
PROXMOX_TYPE=""
ENABLE_COMMANDS="false"
ENROLL="false"
COMMAND_AUTHORITY_SOURCE=""
HEALTH_ADDR=""
HEALTH_ADDR_SET="false"
AGENT_ID=""
HOSTNAME_OVERRIDE=""
REPORT_IP=""
STATE_DIR="/var/lib/pulse-agent"
STATE_DIR_SOURCE="default"
OBSERVERS_FILE=""
KUBE_INCLUDE_ALL_PODS="false"
KUBE_INCLUDE_ALL_DEPLOYMENTS="false"
DISK_EXCLUDES=()
DISK_INCLUDES=()
` + extractInstallShellFunction(t, "strip_recovered_arg_quotes") + `
` + extractInstallShellFunction(t, "normalize_recovered_agent_arg_key") + `
` + extractInstallShellFunction(t, "apply_recovered_agent_arg_value") + `
` + extractInstallShellFunction(t, "recovered_connection_state_ready") + `
` + extractInstallShellFunction(t, "recover_token_from_default_agent_token_file") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_arg_stream") + `
` + extractInstallShellFunction(t, "recover_connection_state_from_env_stream") + `
recover_connection_state_from_arg_stream <<'ARGS'
/usr/local/bin/pulse-agent
--url=https://old-pulse.example.test:7655
--token=deadbeef
--agent-id=agent-123
--hostname=pve-one
--insecure
--cacert=/etc/pulse/old-ca.pem
--server-fingerprint=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ARGS
recover_connection_state_from_env_stream <<'ENV'
PULSE_INSECURE_SKIP_VERIFY=true
PULSE_CACERT=/etc/pulse/older-ca.pem
PULSE_SERVER_FINGERPRINT=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
ENV
printf 'URL=%s\nTOKEN=%s\nAGENT_ID=%s\nHOSTNAME=%s\nINSECURE=%s\nFINGERPRINT=%s\nCACERT=%s\n' \
"$PULSE_URL" "$PULSE_TOKEN" "$AGENT_ID" "$HOSTNAME_OVERRIDE" "$INSECURE" "$SERVER_FINGERPRINT" "$CURL_CA_BUNDLE"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := string(out)
for _, needle := range []string{
"URL=https://new-pulse.example.test:7655",
"TOKEN=deadbeef",
"AGENT_ID=agent-123",
"HOSTNAME=pve-one",
"INSECURE=false",
"FINGERPRINT=",
"CACERT=",
} {
if !strings.Contains(got, needle) {
t.Fatalf("retarget service recovery missing %q:\n%s", needle, got)
}
}
if strings.Contains(got, "old-pulse") || strings.Contains(got, "old-ca") || strings.Contains(got, "aaaaaaaa") || strings.Contains(got, "bbbbbbbb") {
t.Fatalf("retarget carried old service trust or URL:\n%s", got)
}
}
func TestInstallSHRecoversV5ProcessArgsForSavedStateUpdate(t *testing.T) {
script := `
fail() { echo "FAIL:$1"; exit 99; }