diff --git a/README.md b/README.md index d550e2f..19ebdc4 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Linux releases continue to use `proxy-agent:` for backward compatibilit ### Installing behind a network proxy -The installers and the `upgrade` command use a proxy for their web requests: downloading the management script, listing image tags, and fetching the agent configuration from Fivetran. +The installers and the `upgrade` and `update-manager` commands use a proxy for their web requests: downloading the management script, listing image tags, and fetching the agent configuration from Fivetran. On Linux, the shell scripts use `curl`. Set `HTTPS_PROXY` or `https_proxy` for HTTPS URLs, which the scripts use by default. Set lowercase `http_proxy` for HTTP URLs; curl ignores uppercase `HTTP_PROXY`. `NO_PROXY` or `no_proxy` excludes hosts. @@ -122,7 +122,7 @@ $HOME/fivetran-proxy-agent/ Use `proxy-agent-manager.sh` to control the agent: ```bash -./proxy-agent-manager.sh {start|stop|restart|upgrade|status|logs} +./proxy-agent-manager.sh {start|stop|restart|upgrade|update-manager|status|logs} ``` ### Windows @@ -130,7 +130,7 @@ Use `proxy-agent-manager.sh` to control the agent: Use `proxy-agent-manager.ps1` to control the agent: ```powershell -& "$env:USERPROFILE\fivetran-proxy-agent\proxy-agent-manager.ps1" {start|stop|restart|upgrade|status|logs} +& "$env:USERPROFILE\fivetran-proxy-agent\proxy-agent-manager.ps1" {start|stop|restart|upgrade|update-manager|status|logs} ``` | Command | Description | @@ -138,10 +138,76 @@ Use `proxy-agent-manager.ps1` to control the agent: | `start` | Start the agent container | | `stop` | Stop and remove the agent container | | `restart` | Stop then start the agent container | -| `upgrade` | Pull and start the latest version, with auto-rollback on failure | +| `upgrade` | Pull and start the latest agent version, with auto-rollback on failure | +| `update-manager` | Download and replace the management script without restarting the agent | | `status` | Show container name, image, and health status | | `logs` | Stream live container logs | +### Updating the management script + +Run `update-manager` as the user who installed the agent: + +```bash +"$HOME/fivetran-proxy-agent/proxy-agent-manager.sh" update-manager +``` + +```powershell +& "$env:USERPROFILE\fivetran-proxy-agent\proxy-agent-manager.ps1" update-manager +``` + +Use your installation directory if it differs from the default. This downloads the latest management script from this repository's `main` branch, checks that it is nonempty and has valid syntax, then replaces the installed script. A failed download or validation leaves the existing script intact. Configuration, settings, the pinned agent version, and the running container are unchanged; the new script takes effect on the next command. Updating the manager does not require Docker or valid agent configuration. + +`upgrade` updates only the agent container. To update both, run `update-manager` first, then invoke the script again with `upgrade`. + +#### Older installations without `update-manager` + +Replace the script manually once using the commands below. Set `agent_dir` or `$agentDir` to your installation directory if needed. These commands download to a temporary file and validate it before replacing the installed script; no agent restart is needed. + +Linux (Bash): + +```bash +( + set -e + agent_dir="$HOME/fivetran-proxy-agent" + tmp_script=$(mktemp "$agent_dir/.proxy-agent-manager.XXXXXX") + trap 'rm -f "$tmp_script"' EXIT + curl -fSsL --max-time 30 \ + https://raw.githubusercontent.com/fivetran/proxy_agent/main/proxy-agent-manager.sh \ + -o "$tmp_script" + test -s "$tmp_script" + bash -n "$tmp_script" + chmod u+x "$tmp_script" + mv -f "$tmp_script" "$agent_dir/proxy-agent-manager.sh" +) +``` + +Windows (PowerShell): + +```powershell +& { + $ErrorActionPreference = 'Stop' + $agentDir = "$env:USERPROFILE\fivetran-proxy-agent" + $tmpScript = Join-Path $agentDir ('.proxy-agent-manager.' + [guid]::NewGuid() + '.ps1') + try { + Invoke-WebRequest -UseBasicParsing -TimeoutSec 30 ` + -Uri https://raw.githubusercontent.com/fivetran/proxy_agent/main/proxy-agent-manager.ps1 ` + -OutFile $tmpScript + if ([string]::IsNullOrWhiteSpace((Get-Content -LiteralPath $tmpScript -Raw))) { + throw 'Downloaded script is empty.' + } + $parseErrors = $null + $null = [System.Management.Automation.Language.Parser]::ParseFile($tmpScript, [ref]$null, [ref]$parseErrors) + if ($parseErrors.Count -gt 0) { throw 'Downloaded script has invalid syntax.' } + Unblock-File -LiteralPath $tmpScript + [System.IO.File]::Replace($tmpScript, (Join-Path $agentDir 'proxy-agent-manager.ps1'), [NullString]::Value) + } finally { + Remove-Item -LiteralPath $tmpScript -Force -ErrorAction SilentlyContinue + } +} +``` + +The manual PowerShell download uses your system proxy settings; on Windows PowerShell 5.1, pass `-Proxy` and, if needed, `-ProxyUseDefaultCredentials` to `Invoke-WebRequest` when a proxy is required. The `update-manager` command itself uses the environment proxy settings described above. + ## Troubleshooting ### Windows @@ -183,7 +249,7 @@ The scripts read these environment variables so the tests can point them at the |----------|---------|----------| | `PROXY_AGENT_IMAGE` | management scripts | the agent image repository | | `PROXY_AGENT_REGISTRY_TAGS_URL` | installers and management scripts | the registry tag listing URL | -| `PROXY_AGENT_SCRIPT_URL` | installers | the management script download URL | +| `PROXY_AGENT_SCRIPT_URL` | installers and management scripts | the management script download URL | ## License diff --git a/proxy-agent-manager.ps1 b/proxy-agent-manager.ps1 index af04653..ec82576 100644 --- a/proxy-agent-manager.ps1 +++ b/proxy-agent-manager.ps1 @@ -18,51 +18,11 @@ $SETTINGS_FILE = Join-Path $BASE_DIR 'settings.ps1' # Appended to the plain agent version to form the Linux image tag. $LINUX_IMAGE_TAG_SUFFIX = '-ubuntu-26.04' -# settings.ps1 may set $MEMORY_ALLOCATION_MB and, for Windows containers, -# $WINDOWS_VERSION (an LTSC variant that overrides host detection). -if (Test-Path -LiteralPath $SETTINGS_FILE) { - . $SETTINGS_FILE -} -# Unset or empty only, so 0 is rejected like any other invalid setting. -if ("$MEMORY_ALLOCATION_MB" -eq '') { $MEMORY_ALLOCATION_MB = 5120 } -if (-not $WINDOWS_VERSION) { $WINDOWS_VERSION = $null } - # ── Startup validation ─────────────────────────────────────────────────────── -$validCommands = @('start', 'stop', 'restart', 'upgrade', 'status', 'logs') +$validCommands = @('start', 'stop', 'restart', 'upgrade', 'update-manager', 'status', 'logs') if (-not $Command -or $Command -notin $validCommands) { - Write-Host "Usage: $($MyInvocation.MyCommand.Name) {start|stop|restart|upgrade|status|logs}" - exit 1 -} - -if (-not (Test-Path $CONFIG_FILE)) { - Write-Host "ERROR: Config file not found: $CONFIG_FILE" -ForegroundColor Red - exit 1 -} - -try { - $configJson = Get-Content $CONFIG_FILE -Raw | ConvertFrom-Json -} catch { - Write-Host "ERROR: Failed to parse config file: $_" -ForegroundColor Red - exit 1 -} - -$AGENT_ID = $configJson.agent_id -if (-not $AGENT_ID -or $AGENT_ID -eq 'null') { - Write-Host "ERROR: 'agent_id' missing in $CONFIG_FILE" -ForegroundColor Red - exit 1 -} - -$CONTAINER_NAME = "proxy-agent-$AGENT_ID" - -if (-not (Test-Path $VERSION_FILE)) { - Write-Host "ERROR: $VERSION_FILE not found." -ForegroundColor Red - exit 1 -} - -$PINNED_VERSION = (Get-Content $VERSION_FILE -Raw).Trim() -if (-not $PINNED_VERSION) { - Write-Host "ERROR: $VERSION_FILE is empty." -ForegroundColor Red + Write-Host "Usage: $($MyInvocation.MyCommand.Name) {start|stop|restart|upgrade|update-manager|status|logs}" exit 1 } @@ -143,6 +103,34 @@ function Set-WebProxyFromEnvironment { Write-Log "Using proxy $proxyAddress from the environment" } +# Validate the download before atomically replacing the installed script. +function Update-Manager { + $scriptUrl = if ($env:PROXY_AGENT_SCRIPT_URL) { $env:PROXY_AGENT_SCRIPT_URL } else { 'https://raw.githubusercontent.com/fivetran/proxy_agent/main/proxy-agent-manager.ps1' } + $tmpScript = Join-Path $BASE_DIR ('.proxy-agent-manager.' + [guid]::NewGuid() + '.ps1') + try { + Set-WebProxyFromEnvironment -ProbeUri ([Uri]$scriptUrl) + Write-Host "Downloading management script..." + Invoke-WebRequest -Uri $scriptUrl -OutFile $tmpScript -UseBasicParsing -TimeoutSec 30 + if ([string]::IsNullOrWhiteSpace((Get-Content -LiteralPath $tmpScript -Raw))) { + throw 'Downloaded management script is empty.' + } + $parseErrors = $null + $null = [System.Management.Automation.Language.Parser]::ParseFile($tmpScript, [ref]$null, [ref]$parseErrors) + if ($parseErrors.Count -gt 0) { + throw 'Downloaded management script has invalid syntax.' + } + Unblock-File -LiteralPath $tmpScript + # $null binds to an empty string here; Replace requires a null backup path. + [System.IO.File]::Replace($tmpScript, $PSCommandPath, [NullString]::Value) + Write-Host "Management script updated. The agent has not been restarted." + } catch { + Write-Host "ERROR: Failed to update management script: $_" -ForegroundColor Red + exit 1 + } finally { + Remove-Item -LiteralPath $tmpScript -Force -ErrorAction SilentlyContinue + } +} + function Get-ContainerOsType { $osTypeOutput = docker info --format '{{.OSType}}' 2>$null if ($LASTEXITCODE -ne 0) { @@ -463,6 +451,52 @@ function Invoke-Upgrade { # ── Commands ───────────────────────────────────────────────────────────────── +# Updating the manager does not require agent configuration or Docker. +if ($Command -eq 'update-manager') { + Update-Manager + exit 0 +} + +# settings.ps1 may set $MEMORY_ALLOCATION_MB and, for Windows containers, +# $WINDOWS_VERSION (an LTSC variant that overrides host detection). +if (Test-Path -LiteralPath $SETTINGS_FILE) { + . $SETTINGS_FILE +} +# Unset or empty only, so 0 is rejected like any other invalid setting. +if ("$MEMORY_ALLOCATION_MB" -eq '') { $MEMORY_ALLOCATION_MB = 5120 } +if (-not $WINDOWS_VERSION) { $WINDOWS_VERSION = $null } + +if (-not (Test-Path $CONFIG_FILE)) { + Write-Host "ERROR: Config file not found: $CONFIG_FILE" -ForegroundColor Red + exit 1 +} + +try { + $configJson = Get-Content $CONFIG_FILE -Raw | ConvertFrom-Json +} catch { + Write-Host "ERROR: Failed to parse config file: $_" -ForegroundColor Red + exit 1 +} + +$AGENT_ID = $configJson.agent_id +if (-not $AGENT_ID -or $AGENT_ID -eq 'null') { + Write-Host "ERROR: 'agent_id' missing in $CONFIG_FILE" -ForegroundColor Red + exit 1 +} + +$CONTAINER_NAME = "proxy-agent-$AGENT_ID" + +if (-not (Test-Path $VERSION_FILE)) { + Write-Host "ERROR: $VERSION_FILE not found." -ForegroundColor Red + exit 1 +} + +$PINNED_VERSION = (Get-Content $VERSION_FILE -Raw).Trim() +if (-not $PINNED_VERSION) { + Write-Host "ERROR: $VERSION_FILE is empty." -ForegroundColor Red + exit 1 +} + switch ($Command) { 'start' { $CURRENT_VERSION = Get-ReleaseVersion -Tag $PINNED_VERSION diff --git a/proxy-agent-manager.sh b/proxy-agent-manager.sh index c28c14a..d1c47a0 100755 --- a/proxy-agent-manager.sh +++ b/proxy-agent-manager.sh @@ -2,7 +2,7 @@ # # Management script for Fivetran Proxy Agent (Docker) # -# Usage: ./proxy-agent-manager.sh {start|stop|restart|upgrade|status|logs} +# Usage: ./proxy-agent-manager.sh {start|stop|restart|upgrade|update-manager|status|logs} # set -euo pipefail @@ -23,30 +23,6 @@ get_agent_id() { || true } -if [ ! -f "$CONFIG_FILE" ]; then - echo "ERROR: Config file not found: $CONFIG_FILE" - exit 1 -fi - -AGENT_ID=$(get_agent_id) -if [ -z "$AGENT_ID" ] || [ "$AGENT_ID" = "null" ]; then - echo "ERROR: 'agent_id' missing in $CONFIG_FILE" - exit 1 -fi - -CONTAINER_NAME="proxy-agent-${AGENT_ID}" - -if [ ! -f "$VERSION_FILE" ]; then - echo "ERROR: $VERSION_FILE not found." - exit 1 -fi -# Also strips the UTF-8 byte order mark an editor on Windows may add. -PINNED_VERSION=$(LC_ALL=C tr -d '[:space:]\357\273\277' < "$VERSION_FILE") -if [ -z "$PINNED_VERSION" ]; then - echo "ERROR: $VERSION_FILE is empty." - exit 1 -fi - LOGFILE="$BASE_DIR/logs/proxy-agent-manager.log" CONTAINER_LOG_DIR="/app/logs" @@ -58,12 +34,6 @@ log() { echo "$(date -u +'%Y-%m-%d %H:%M:%S') UTC - $1" >> "$LOGFILE" } -if [ -f "$SETTINGS_FILE" ]; then - # shellcheck disable=SC1090 - source "$SETTINGS_FILE" -fi -MEMORY_ALLOCATION_MB="${MEMORY_ALLOCATION_MB:-5120}" - # Only the commands that start a container check the setting, so stop, status # and logs still work with an invalid one. check_memory_allocation() { @@ -73,6 +43,28 @@ check_memory_allocation() { fi } +# Download beside the installed script so rename replaces it atomically. +# The subshell confines the cleanup trap to this command. +update_manager() ( + local script_url="${PROXY_AGENT_SCRIPT_URL:-https://raw.githubusercontent.com/fivetran/proxy_agent/main/proxy-agent-manager.sh}" + local tmp_script + tmp_script=$(mktemp "$BASE_DIR/.proxy-agent-manager.XXXXXX") + trap 'rm -f "$tmp_script"' EXIT + + echo "Downloading management script..." + if ! curl -fSsL --max-time 30 "$script_url" -o "$tmp_script"; then + echo "ERROR: Failed to download management script" >&2 + exit 1 + fi + if [ ! -s "$tmp_script" ] || ! bash -n "$tmp_script"; then + echo "ERROR: Downloaded management script is empty or has invalid syntax" >&2 + exit 1 + fi + chmod u+x "$tmp_script" + mv -f "$tmp_script" "$0" + echo "Management script updated. The agent has not been restarted." +) + get_latest_version() { local registry_host="${IMAGE%%/*}" local repository_path="${IMAGE#*/}" @@ -201,6 +193,43 @@ start_agent() { # ── Commands ───────────────────────────────────────────────────────────────── +# Updating the manager does not require agent configuration or Docker. Exit in +# this parsed block so Bash never resumes reading the replaced script. +if [ "${1:-}" = "update-manager" ]; then + update_manager + exit 0 +fi + +if [ ! -f "$CONFIG_FILE" ]; then + echo "ERROR: Config file not found: $CONFIG_FILE" + exit 1 +fi + +AGENT_ID=$(get_agent_id) +if [ -z "$AGENT_ID" ] || [ "$AGENT_ID" = "null" ]; then + echo "ERROR: 'agent_id' missing in $CONFIG_FILE" + exit 1 +fi + +CONTAINER_NAME="proxy-agent-${AGENT_ID}" + +if [ ! -f "$VERSION_FILE" ]; then + echo "ERROR: $VERSION_FILE not found." + exit 1 +fi +# Also strips the UTF-8 byte order mark an editor on Windows may add. +PINNED_VERSION=$(LC_ALL=C tr -d '[:space:]\357\273\277' < "$VERSION_FILE") +if [ -z "$PINNED_VERSION" ]; then + echo "ERROR: $VERSION_FILE is empty." + exit 1 +fi + +if [ -f "$SETTINGS_FILE" ]; then + # shellcheck disable=SC1090 + source "$SETTINGS_FILE" +fi +MEMORY_ALLOCATION_MB="${MEMORY_ALLOCATION_MB:-5120}" + case "${1:-}" in start) CURRENT_VERSION=$(get_release_version "$PINNED_VERSION") || exit 1 @@ -238,7 +267,7 @@ case "${1:-}" in ;; *) - echo "Usage: $0 {start|stop|restart|upgrade|status|logs}" + echo "Usage: $0 {start|stop|restart|upgrade|update-manager|status|logs}" exit 1 ;; esac diff --git a/tests/bash/manager.bats b/tests/bash/manager.bats index 8fa0bde..394cda1 100644 --- a/tests/bash/manager.bats +++ b/tests/bash/manager.bats @@ -12,6 +12,7 @@ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" IMAGE_REPO="us-docker.pkg.dev/prod-eng-fivetran-public-repos/public-docker-us/proxy-agent" setup() { + unset PROXY_AGENT_SCRIPT_URL STUB_CURL_EXIT_CODE AGENT_DIR="$(mktemp -d)" mkdir -p "$AGENT_DIR/config" cp "$REPO_ROOT/proxy-agent-manager.sh" "$AGENT_DIR/" @@ -44,6 +45,70 @@ version_file() { cat "$AGENT_DIR/version" } +@test "update-manager replaces the script without changing the agent or its files" { + manager start + cp "$DOCKER_LOG" "$AGENT_DIR/docker.before" + echo 'MEMORY_ALLOCATION_MB=2048' > "$AGENT_DIR/settings.sh" + export STUB_CURL_RESPONSE + STUB_CURL_RESPONSE="$(cat "$REPO_ROOT/proxy-agent-manager.sh")"$'\n# Updated manager\n' + + run manager update-manager + + [ "$status" -eq 0 ] + [[ "$output" == *"Management script updated"* ]] + [ "$(cat "$AGENT_DIR/proxy-agent-manager.sh")" = "${STUB_CURL_RESPONSE%$'\n'}" ] + [ -x "$AGENT_DIR/proxy-agent-manager.sh" ] + cmp "$DOCKER_LOG" "$AGENT_DIR/docker.before" + [ "$(version_file)" = "1.2.9" ] + [ "$(cat "$AGENT_DIR/settings.sh")" = 'MEMORY_ALLOCATION_MB=2048' ] + [ "$(cat "$AGENT_DIR/config/config.json")" = '{"agent_id": "abc123"}' ] + grep -q 'https://raw.githubusercontent.com/fivetran/proxy_agent/main/proxy-agent-manager.sh' "$CURL_LOG" + grep -q -- '--max-time 30' "$CURL_LOG" + [ -z "$(find "$AGENT_DIR" -name '.proxy-agent-manager.*' -print)" ] + + run manager status + [ "$status" -eq 0 ] + [[ "$output" == *"NAMES IMAGE STATUS"* ]] +} + +@test "update-manager supports a custom URL without config, version, or valid settings" { + rm "$AGENT_DIR/config/config.json" "$AGENT_DIR/version" + echo 'exit 99' > "$AGENT_DIR/settings.sh" + export PROXY_AGENT_SCRIPT_URL='http://127.0.0.1:8080/manager.sh' + export STUB_CURL_RESPONSE=$'#!/usr/bin/env bash\nexit 99\n' + + run manager update-manager + + [ "$status" -eq 0 ] + [ ! -f "$DOCKER_LOG" ] + grep -q "$PROXY_AGENT_SCRIPT_URL" "$CURL_LOG" +} + +@test "update-manager preserves the old script and cleans up after a partial failed download" { + export STUB_CURL_RESPONSE=$'#!/usr/bin/env bash\n# Partial download' + export STUB_CURL_EXIT_CODE=18 + + run manager update-manager + + [ "$status" -eq 1 ] + [[ "$output" == *"Failed to download management script"* ]] + cmp "$REPO_ROOT/proxy-agent-manager.sh" "$AGENT_DIR/proxy-agent-manager.sh" + [ -z "$(find "$AGENT_DIR" -name '.proxy-agent-manager.*' -print)" ] + [ ! -f "$DOCKER_LOG" ] +} + +@test "update-manager rejects empty and syntactically invalid downloads" { + for response in '' $'#!/usr/bin/env bash\nif then'; do + export STUB_CURL_RESPONSE="$response" + run manager update-manager + [ "$status" -eq 1 ] + [[ "$output" == *"empty or has invalid syntax"* ]] + cmp "$REPO_ROOT/proxy-agent-manager.sh" "$AGENT_DIR/proxy-agent-manager.sh" + [ -z "$(find "$AGENT_DIR" -name '.proxy-agent-manager.*' -print)" ] + [ ! -f "$DOCKER_LOG" ] + done +} + @test "start runs the pinned Ubuntu image with the default memory limit" { run manager start [ "$status" -eq 0 ] diff --git a/tests/bash/stubs/curl b/tests/bash/stubs/curl index fddcf6e..e780938 100755 --- a/tests/bash/stubs/curl +++ b/tests/bash/stubs/curl @@ -4,4 +4,12 @@ # answers every call with $STUB_CURL_RESPONSE and never touches the network. # echo "$*" >> "$CURL_LOG" -echo "$STUB_CURL_RESPONSE" +while [ "$#" -gt 0 ]; do + if [ "$1" = "-o" ]; then + printf '%s' "$STUB_CURL_RESPONSE" > "$2" + exit "${STUB_CURL_EXIT_CODE:-0}" + fi + shift +done +printf '%s\n' "$STUB_CURL_RESPONSE" +exit "${STUB_CURL_EXIT_CODE:-0}" diff --git a/tests/pwsh/Manager.Tests.ps1 b/tests/pwsh/Manager.Tests.ps1 index 2058cf6..b540f9e 100644 --- a/tests/pwsh/Manager.Tests.ps1 +++ b/tests/pwsh/Manager.Tests.ps1 @@ -109,6 +109,17 @@ BeforeAll { return [pscustomobject]@{ tags = $global:ProxyAgentStub.Tags } } + # Write even on failure to simulate a partially downloaded script. + function Invoke-WebRequest { + param([string]$Uri, [string]$OutFile, [switch]$UseBasicParsing, [int]$TimeoutSec) + $global:ProxyAgentStub.DownloadUrl = $Uri + $global:ProxyAgentStub.DownloadTimeoutSec = $TimeoutSec + $global:ProxyAgentStub.DownloadUseBasicParsing = $UseBasicParsing.IsPresent + $global:ProxyAgentStub.DownloadProxy = [System.Net.WebRequest]::DefaultWebProxy.GetProxy([Uri]$Uri) + [System.IO.File]::WriteAllText($OutFile, $global:ProxyAgentStub.ScriptContent, [System.Text.UTF8Encoding]::new($true)) + if ($global:ProxyAgentStub.DownloadFails) { throw 'Download interrupted' } + } + # Stand-in for the host OS lookup, reporting $global:ProxyAgentStub.OperatingSystem. function Get-CimInstance { return $global:ProxyAgentStub.OperatingSystem @@ -122,7 +133,7 @@ AfterAll { Describe 'proxy-agent-manager.ps1' { BeforeEach { # A developer's environment must not leak into the default-URL or proxy assertions. - Remove-Item Env:\PROXY_AGENT_REGISTRY_TAGS_URL, Env:\HTTPS_PROXY, Env:\HTTP_PROXY, Env:\NO_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:\PROXY_AGENT_REGISTRY_TAGS_URL, Env:\PROXY_AGENT_SCRIPT_URL, Env:\HTTPS_PROXY, Env:\HTTP_PROXY, Env:\NO_PROXY -ErrorAction SilentlyContinue $script:OriginalWebProxy = [System.Net.WebRequest]::DefaultWebProxy [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy if ($PSVersionTable.PSVersion.Major -ge 7) { @@ -137,13 +148,19 @@ Describe 'proxy-agent-manager.ps1' { RegistryUrl = $null RegistryMethod = $null RegistryTimeoutSec = $null + DownloadUrl = $null + DownloadTimeoutSec = $null + DownloadUseBasicParsing = $false + DownloadProxy = $null + DownloadFails = $false + ScriptContent = (Get-Content -LiteralPath (Join-Path $script:RepoRoot 'proxy-agent-manager.ps1') -Raw) + "`n# Updated manager`n" OperatingSystem = [pscustomobject]@{ ProductType = 3; BuildNumber = 20348; Caption = 'Windows Server 2022' } Tags = @('1.2.9-ubuntu-26.04', '1.2.10-ubuntu-26.04', '1.2.11', '1.2.10-windows-ltsc2022', '1.2.10-windows-ltsc2025') } } AfterEach { - Remove-Item Env:\PROXY_AGENT_REGISTRY_TAGS_URL, Env:\HTTPS_PROXY, Env:\HTTP_PROXY, Env:\NO_PROXY -ErrorAction SilentlyContinue + Remove-Item Env:\PROXY_AGENT_REGISTRY_TAGS_URL, Env:\PROXY_AGENT_SCRIPT_URL, Env:\HTTPS_PROXY, Env:\HTTP_PROXY, Env:\NO_PROXY -ErrorAction SilentlyContinue [System.Net.WebRequest]::DefaultWebProxy = $script:OriginalWebProxy if ($PSVersionTable.PSVersion.Major -ge 7) { [System.Net.Http.HttpClient]::DefaultProxy = $script:OriginalHttpClientProxy @@ -153,6 +170,77 @@ Describe 'proxy-agent-manager.ps1' { } } + Context 'updating the manager' { + It 'replaces the script without changing the agent or its files' { + $manager = New-AgentDir -Settings '$MEMORY_ALLOCATION_MB = 2048' + $null = Invoke-Manager $manager start + $callsBefore = $global:ProxyAgentStub.DockerCalls.Count + $configBefore = Get-Content -LiteralPath (Join-Path $script:AgentDir 'config\config.json') -Raw + $settingsBefore = Get-SettingsFile + + $output = Invoke-Manager $manager update-manager + + $LASTEXITCODE | Should -Be 0 -Because $output + $output | Should -Match 'Management script updated' + Get-Content -LiteralPath $manager -Raw | Should -Be $global:ProxyAgentStub.ScriptContent + $global:ProxyAgentStub.DockerCalls.Count | Should -Be $callsBefore + $global:ProxyAgentStub.RegistryUrl | Should -BeNullOrEmpty + Get-VersionFile | Should -Be '1.2.9' + Get-SettingsFile | Should -Be $settingsBefore + Get-Content -LiteralPath (Join-Path $script:AgentDir 'config\config.json') -Raw | Should -Be $configBefore + $global:ProxyAgentStub.DownloadUrl | Should -Be 'https://raw.githubusercontent.com/fivetran/proxy_agent/main/proxy-agent-manager.ps1' + $global:ProxyAgentStub.DownloadTimeoutSec | Should -Be 30 + $global:ProxyAgentStub.DownloadUseBasicParsing | Should -BeTrue + Get-ChildItem -LiteralPath $script:AgentDir -Force -Filter '.proxy-agent-manager.*' | Should -BeNullOrEmpty + + Invoke-Manager $manager status | Should -Match 'NAMES IMAGE STATUS' + $LASTEXITCODE | Should -Be 0 -Because $output + } + + It 'works without config, version, or valid settings and does not execute the download' { + $manager = New-AgentDir -Settings 'throw "Settings must not be loaded"' + Remove-Item -LiteralPath (Join-Path $script:AgentDir 'config\config.json'), (Join-Path $script:AgentDir 'version') + $global:ProxyAgentStub.ScriptContent = 'throw "Download must not execute"' + + $output = Invoke-Manager $manager update-manager + + $LASTEXITCODE | Should -Be 0 -Because $output + $global:ProxyAgentStub.DockerCalls | Should -BeNullOrEmpty + Get-Content -LiteralPath $manager -Raw | Should -Be $global:ProxyAgentStub.ScriptContent + } + + It 'uses the script URL override and configured web proxy for the download' { + $manager = New-AgentDir + $env:PROXY_AGENT_SCRIPT_URL = 'https://downloads.example.com/manager.ps1' + $env:HTTPS_PROXY = 'http://proxy.example.com:3128' + + $output = Invoke-Manager $manager update-manager + + $LASTEXITCODE | Should -Be 0 -Because $output + $global:ProxyAgentStub.DownloadUrl | Should -Be $env:PROXY_AGENT_SCRIPT_URL + $global:ProxyAgentStub.DownloadProxy.AbsoluteUri | Should -Be 'http://proxy.example.com:3128/' + } + + It 'preserves the old script and cleans up after ' -ForEach @( + @{ Failure = 'a partial failed download'; Content = '# Partial download'; DownloadFails = $true } + @{ Failure = 'an empty download'; Content = ''; DownloadFails = $false } + @{ Failure = 'a syntax error'; Content = 'function Broken {'; DownloadFails = $false } + ) { + $manager = New-AgentDir + $original = Get-Content -LiteralPath $manager -Raw + $global:ProxyAgentStub.ScriptContent = $Content + $global:ProxyAgentStub.DownloadFails = $DownloadFails + + $output = Invoke-Manager $manager update-manager + + $LASTEXITCODE | Should -Be 1 + $output | Should -Match 'Failed to update management script' + Get-Content -LiteralPath $manager -Raw | Should -Be $original + Get-ChildItem -LiteralPath $script:AgentDir -Force -Filter '.proxy-agent-manager.*' | Should -BeNullOrEmpty + $global:ProxyAgentStub.DockerCalls | Should -BeNullOrEmpty + } + } + Context 'with Linux containers' { It 'starts the pinned Ubuntu image with the default memory limit' { $manager = New-AgentDir