diff --git a/README.md b/README.md index f02f740..36e29cb 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,9 @@ To install into a custom directory: $env:RUNTIME = 'docker'; $env:TOKEN = 'YOUR_AGENT_TOKEN'; & .\install.ps1 -InstallDir C:\path\to\dir ``` -The installer detects the Docker container mode. Linux-container mode, including Windows 10/11 with Docker Desktop and WSL2, uses the explicit Ubuntu image tag `-ubuntu-26.04`. Existing Linux installations that pin the legacy numeric tag `` continue to work and are upgraded using the explicit Ubuntu tag. Windows-container mode detects the Windows Server LTSC version and selects the matching image tag. To override detection for a host known to support a specific Windows image, pass `-WindowsVersion ltsc2019`, `-WindowsVersion ltsc2022`, or `-WindowsVersion ltsc2025`. +The installer pins the plain agent version (for example `1.2.9`) in the `version` file, and the management script appends the image tag suffix for the Docker container mode when it starts the container. Linux-container mode, including Windows 10/11 with Docker Desktop and WSL2, uses the Ubuntu image tag `-ubuntu-26.04`. Windows-container mode detects the Windows Server LTSC version and selects the matching `-windows-` image tag. Existing installations whose `version` file contains a full image tag keep working; the file is rewritten to the plain version on the next `start` or `upgrade`. To override detection for a host known to support a specific Windows image, pass `-WindowsVersion ltsc2019`, `-WindowsVersion ltsc2022`, or `-WindowsVersion ltsc2025`. -Windows Server hosts use an exact LTSC mapping: Server 2019 selects `ltsc2019`, Server 2022 selects `ltsc2022`, and Server 2025 selects `ltsc2025`. The `-WindowsVersion` option is an operator-controlled override and does not validate host/image compatibility; confirm the supported Windows container version and required isolation mode before using it. +Windows Server hosts use an exact LTSC mapping: Server 2019 selects `ltsc2019`, Server 2022 selects `ltsc2022`, and Server 2025 selects `ltsc2025`. The `-WindowsVersion` option is an operator-controlled override and does not validate host/image compatibility; confirm the supported Windows container version and required isolation mode before using it. The override is saved as `$WINDOWS_VERSION` in `settings.ps1`, so `upgrade` keeps using it. Linux releases continue to use `proxy-agent:` for backward compatibility and are also published as `proxy-agent:-ubuntu-26.04`. Both tags reference the same Linux multi-architecture image. Windows releases use the explicit `proxy-agent:-windows-ltsc2019`, `proxy-agent:-windows-ltsc2022`, or `proxy-agent:-windows-ltsc2025` tags. @@ -84,7 +84,7 @@ $HOME/fivetran-proxy-agent/ ├── config/ │ └── config.json --> Agent configuration (permissions: 600) ├── logs/ --> Agent and manager logs -└── version --> Pinned agent image tag +└── version --> Pinned agent version (for example 1.2.9) # Windows %USERPROFILE%\fivetran-proxy-agent\ @@ -92,7 +92,7 @@ $HOME/fivetran-proxy-agent/ ├── config\ │ └── config.json --> Agent configuration (owner read/write; SYSTEM and Administrators read) ├── logs\ --> Agent and manager logs -└── version --> Pinned agent image tag +└── version --> Pinned agent version (for example 1.2.9) ``` ## Managing the agent diff --git a/install.ps1 b/install.ps1 index 1b55549..c107644 100644 --- a/install.ps1 +++ b/install.ps1 @@ -15,6 +15,7 @@ param( $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' +if ($WindowsVersion) { $WindowsVersion = $WindowsVersion.ToLowerInvariant() } # ── Constants ──────────────────────────────────────────────────────────────── @@ -240,7 +241,7 @@ function Get-LatestVersion { exit 1 } - return ($versions | Sort-Object { [version]($_ -replace $releasePattern, '') } | Select-Object -Last 1) + return ($versions | ForEach-Object { $_ -replace $releasePattern, '' } | Sort-Object { [version]$_ } | Select-Object -Last 1) } function Get-LatestLinuxVersion { @@ -259,7 +260,7 @@ function Get-LatestLinuxVersion { exit 1 } - return ($versions | Sort-Object { [version]($_ -replace $releasePattern, '') } | Select-Object -Last 1) + return ($versions | ForEach-Object { $_ -replace $releasePattern, '' } | Sort-Object { [version]$_ } | Select-Object -Last 1) } # ── Entry point ────────────────────────────────────────────────────────────── @@ -373,8 +374,13 @@ try { $null = New-Item -ItemType Directory -Force -Path (Join-Path $InstallDir 'config') $null = New-Item -ItemType Directory -Force -Path (Join-Path $InstallDir 'logs') - # Persist chosen container memory for proxy-agent-manager.ps1 to read - Set-Content -LiteralPath (Join-Path $InstallDir $SETTINGS_FILE) -Value "`$MEMORY_ALLOCATION_MB = $memoryMB" -Encoding ascii + # Persist settings for proxy-agent-manager.ps1 to read: the container memory + # and, when given, the Windows image variant override. + $settings = @("`$MEMORY_ALLOCATION_MB = $memoryMB") + if ($WindowsVersion) { + $settings += "`$WINDOWS_VERSION = '$WindowsVersion'" + } + Set-Content -LiteralPath (Join-Path $InstallDir $SETTINGS_FILE) -Value $settings -Encoding ascii # Download management script from public repo (temp file → move for atomicity) Write-Host "Downloading management script..." diff --git a/install.sh b/install.sh index 50c2c3a..4322ea7 100755 --- a/install.sh +++ b/install.sh @@ -229,7 +229,7 @@ get_latest_version() { local version version=$(echo "$tags_json" \ | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+-ubuntu-26\.04"' \ - | tr -d '"' \ + | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' \ | sort -V \ | tail -1 \ || true) diff --git a/proxy-agent-manager.ps1 b/proxy-agent-manager.ps1 index 6eb00a8..b265cb6 100644 --- a/proxy-agent-manager.ps1 +++ b/proxy-agent-manager.ps1 @@ -15,12 +15,17 @@ $VERSION_FILE = Join-Path $BASE_DIR 'version' $LOGFILE = Join-Path $BASE_DIR 'logs\proxy-agent-manager.log' $CONTAINER_LOG_DIR = '/app/logs' $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 ─────────────────────────────────────────────────────── @@ -55,8 +60,8 @@ if (-not (Test-Path $VERSION_FILE)) { exit 1 } -$CURRENT_VERSION = (Get-Content $VERSION_FILE -Raw).Trim() -if (-not $CURRENT_VERSION) { +$PINNED_VERSION = (Get-Content $VERSION_FILE -Raw).Trim() +if (-not $PINNED_VERSION) { Write-Host "ERROR: $VERSION_FILE is empty." -ForegroundColor Red exit 1 } @@ -105,65 +110,71 @@ function Get-WindowsLtscVersion { throw "Unsupported Windows Server build '$($operatingSystem.BuildNumber)' ('$($operatingSystem.Caption)')." } -function Get-ImageTagType { +# Returns the plain agent version for a version file entry. Accepts a plain +# version (1.2.9) or a legacy image tag (1.2.9-ubuntu-26.04, 1.2.9-windows-ltsc2022). +function Get-ReleaseVersion { param([Parameter(Mandatory)][string]$Tag) - if ($Tag -match '^\d+\.\d+\.\d+-windows-(?:ltsc2019|ltsc2022|ltsc2025)$') { - return 'windows' - } - if ($Tag -match '^\d+\.\d+\.\d+-ubuntu-26\.04$') { - return 'linux' - } - if ($Tag -match '^\d+\.\d+\.\d+$') { - return 'legacy-linux' + if ($Tag -match '^(?\d+\.\d+\.\d+)(?:-ubuntu-26\.04|-windows-(?:ltsc2019|ltsc2022|ltsc2025))?$') { + return $Matches.version } - throw "Invalid proxy-agent image tag '$Tag'." + throw "Invalid proxy agent version '$Tag' in $VERSION_FILE. Expected ..." } -function Get-ImageTagVariant { - param([Parameter(Mandatory)][string]$Tag) +# Returns the LTSC variant to use for Windows containers, or $null for Linux +# containers. A variant pinned by a legacy Windows image tag in the version file +# wins and is saved to settings.ps1, then the $WINDOWS_VERSION setting, then the +# variant detected from the host. +function Resolve-WindowsVersion { + param([Parameter(Mandatory)][ValidateSet('linux', 'windows')][string]$ContainerOsType) - if ($Tag -match '^\d+\.\d+\.\d+-windows-(?ltsc2019|ltsc2022|ltsc2025)$') { - return $Matches.variant + $pinnedWindowsVariant = $null + if ($PINNED_VERSION -match '-windows-(?ltsc2019|ltsc2022|ltsc2025)$') { + $pinnedWindowsVariant = $Matches.variant } - - throw "Invalid Windows proxy-agent image tag '$Tag'. Expected -windows-ltsc2019, -windows-ltsc2022, or -windows-ltsc2025." -} - -function Resolve-ImageTag { - param( - [Parameter(Mandatory)][string]$Tag, - [Parameter(Mandatory)][ValidateSet('linux', 'windows')][string]$ContainerOsType - ) - - $tagType = Get-ImageTagType -Tag $Tag - - if ($Tag -match '^\d+\.\d+\.\d+$') { - if ($ContainerOsType -eq 'windows') { - return "$Tag-windows-$(Get-WindowsLtscVersion)" + if ($ContainerOsType -ne 'windows') { + if ($pinnedWindowsVariant) { + throw "Windows proxy-agent image tags require Docker to run Windows containers." } - return $Tag + return $null } - - if ($tagType -eq 'windows' -and $ContainerOsType -ne 'windows') { - throw "Windows proxy-agent image tags require Docker to run Windows containers." + if ($PINNED_VERSION -match '-ubuntu-26\.04$') { + Write-Log "WARNING: $VERSION_FILE pins the Linux image tag $PINNED_VERSION, but Docker is running Windows containers. Using the Windows image for version $CURRENT_VERSION instead." } - if ($tagType -eq 'linux' -and $ContainerOsType -ne 'linux') { - throw "Linux proxy-agent image tags require Docker to run Linux containers." + if ($pinnedWindowsVariant) { + $legacyVariant = $pinnedWindowsVariant + if ($WINDOWS_VERSION -ne $legacyVariant) { + Add-Content -Path $SETTINGS_FILE -Value "`n`$WINDOWS_VERSION = '$legacyVariant'" -Encoding ascii + $script:WINDOWS_VERSION = $legacyVariant + } + return $legacyVariant } - - return $Tag + if ($WINDOWS_VERSION) { + return $WINDOWS_VERSION + } + return Get-WindowsLtscVersion } -function Get-ReleaseVersion { - param([Parameter(Mandatory)][string]$Tag) +function Get-ImageTag { + param( + [Parameter(Mandatory)][string]$Version, + [Parameter(Mandatory)][ValidateSet('linux', 'windows')][string]$ContainerOsType, + [string]$WindowsVersion + ) - if ($Tag -match '^(?\d+\.\d+\.\d+)(?:(?:-ubuntu-26\.04)|(?:-windows-(?:ltsc2019|ltsc2022|ltsc2025)))?$') { - return $Matches.version + if ($ContainerOsType -eq 'windows') { + return "$Version-windows-$WindowsVersion" } + return "$Version$LINUX_IMAGE_TAG_SUFFIX" +} - throw "Invalid proxy-agent image tag '$Tag'." +function Set-PinnedVersion { + param([Parameter(Mandatory)][string]$Version) + if ($Version -ne $PINNED_VERSION) { + Set-Content -Path $VERSION_FILE -Value $Version -Encoding ascii + $script:PINNED_VERSION = $Version + } } function Get-LatestVersion { @@ -204,15 +215,12 @@ function Get-LatestVersion { exit 1 } - $latest = $versions | Sort-Object { [version]($_ -replace $releasePattern, '') } | Select-Object -Last 1 - return $latest + return ($versions | ForEach-Object { $_ -replace $releasePattern, '' } | Sort-Object { [version]$_ } | Select-Object -Last 1) } function Compare-SemVer { param([string]$A, [string]$B) - $aVer = [version](Get-ReleaseVersion -Tag $A) - $bVer = [version](Get-ReleaseVersion -Tag $B) - return $aVer.CompareTo($bVer) + return ([version]$A).CompareTo([version]$B) } # Only the commands that start a container check the setting, so stop, status @@ -235,10 +243,10 @@ function Stop-ProxyAgent { function Start-ProxyAgent { param( - [Parameter(Mandatory)][string]$Version, + [Parameter(Mandatory)][string]$ImageTag, [Parameter(Mandatory)][ValidateSet('linux', 'windows')][string]$ContainerOsType ) - Write-Log "Starting $CONTAINER_NAME (version: $Version)..." + Write-Log "Starting $CONTAINER_NAME (image tag: $ImageTag)..." Stop-ProxyAgent @@ -283,7 +291,7 @@ function Start-ProxyAgent { '--health-timeout', '15s', '-v', "${configDirectoryMount}:C:/config:ro", '-v', "${logsMount}:$containerLogDir", - "${IMAGE}:${Version}", + "${IMAGE}:${ImageTag}", '-i', $containerConfigPath ) } else { @@ -296,7 +304,7 @@ function Start-ProxyAgent { '--health-timeout', '3s', '-v', "${configFileMount}:/config/config.json:ro", '-v', "${logsMount}:$containerLogDir", - "${IMAGE}:${Version}", + "${IMAGE}:${ImageTag}", '-i', $containerConfigPath ) } @@ -348,55 +356,39 @@ function Start-ProxyAgent { function Start-CurrentProxyAgent { $containerOsType = Get-ContainerOsType - $resolvedVersion = Resolve-ImageTag -Tag $CURRENT_VERSION -ContainerOsType $containerOsType + $windowsVersion = Resolve-WindowsVersion -ContainerOsType $containerOsType + $imageTag = Get-ImageTag -Version $CURRENT_VERSION -ContainerOsType $containerOsType -WindowsVersion $windowsVersion + Set-PinnedVersion $CURRENT_VERSION - if (-not (Start-ProxyAgent -Version $resolvedVersion -ContainerOsType $containerOsType)) { + if (-not (Start-ProxyAgent -ImageTag $imageTag -ContainerOsType $containerOsType)) { return $false } - if ($resolvedVersion -ne $CURRENT_VERSION) { - Set-Content -Path $VERSION_FILE -Value $resolvedVersion -Encoding ascii - } - return $true } function Invoke-Upgrade { Write-Host "Checking for latest version..." $containerOsType = Get-ContainerOsType - $currentVersion = Resolve-ImageTag -Tag $CURRENT_VERSION -ContainerOsType $containerOsType - if ($containerOsType -eq 'windows') { - $windowsVersion = Get-ImageTagVariant -Tag $currentVersion - $latestVersion = Get-LatestVersion -ContainerOsType windows -WindowsVersion $windowsVersion - } else { - $tagType = Get-ImageTagType -Tag $currentVersion - if ($tagType -eq 'windows') { - throw "Windows proxy-agent image tags require Docker to run Windows containers." - } - $latestVersion = Get-LatestVersion -ContainerOsType linux - } - - if ((Compare-SemVer $latestVersion $currentVersion) -eq 0) { - if ($currentVersion -ne $CURRENT_VERSION) { - Write-Log "Switching image tag from $CURRENT_VERSION to $currentVersion..." - if (-not (Start-ProxyAgent -Version $currentVersion -ContainerOsType $containerOsType)) { - exit 1 - } - Set-Content -Path $VERSION_FILE -Value $currentVersion -Encoding ascii - } + $windowsVersion = Resolve-WindowsVersion -ContainerOsType $containerOsType + $latestVersion = Get-LatestVersion -ContainerOsType $containerOsType -WindowsVersion $windowsVersion - Write-Host "Already running the latest version ($currentVersion)." + if ((Compare-SemVer $latestVersion $CURRENT_VERSION) -eq 0) { + Set-PinnedVersion $CURRENT_VERSION + Write-Host "Already running the latest version ($CURRENT_VERSION)." exit 0 } - Write-Log "Upgrading from $currentVersion to $latestVersion..." - Set-Content -Path $VERSION_FILE -Value $latestVersion -Encoding ascii + Write-Log "Upgrading from $CURRENT_VERSION to $latestVersion..." + Set-PinnedVersion $latestVersion - if (-not (Start-ProxyAgent -Version $latestVersion -ContainerOsType $containerOsType)) { - Write-Log "Upgrade failed, rolling back to $currentVersion..." - Set-Content -Path $VERSION_FILE -Value $currentVersion -Encoding ascii - if (-not (Start-ProxyAgent -Version $currentVersion -ContainerOsType $containerOsType)) { - Write-Log "Rollback to $currentVersion failed." + $latestTag = Get-ImageTag -Version $latestVersion -ContainerOsType $containerOsType -WindowsVersion $windowsVersion + if (-not (Start-ProxyAgent -ImageTag $latestTag -ContainerOsType $containerOsType)) { + Write-Log "Upgrade failed, rolling back to $CURRENT_VERSION..." + Set-PinnedVersion $CURRENT_VERSION + $currentTag = Get-ImageTag -Version $CURRENT_VERSION -ContainerOsType $containerOsType -WindowsVersion $windowsVersion + if (-not (Start-ProxyAgent -ImageTag $currentTag -ContainerOsType $containerOsType)) { + Write-Log "Rollback to $CURRENT_VERSION failed." } exit 1 } @@ -406,6 +398,7 @@ function Invoke-Upgrade { switch ($Command) { 'start' { + $CURRENT_VERSION = Get-ReleaseVersion -Tag $PINNED_VERSION Assert-MemoryAllocation if (-not (Start-CurrentProxyAgent)) { exit 1 } } @@ -414,11 +407,13 @@ switch ($Command) { Stop-ProxyAgent } 'restart' { + $CURRENT_VERSION = Get-ReleaseVersion -Tag $PINNED_VERSION Assert-MemoryAllocation Write-Log "Restarting $CONTAINER_NAME..." if (-not (Start-CurrentProxyAgent)) { exit 1 } } 'upgrade' { + $CURRENT_VERSION = Get-ReleaseVersion -Tag $PINNED_VERSION Assert-MemoryAllocation Invoke-Upgrade } diff --git a/proxy-agent-manager.sh b/proxy-agent-manager.sh index 75ecbb7..c28c14a 100755 --- a/proxy-agent-manager.sh +++ b/proxy-agent-manager.sh @@ -11,6 +11,8 @@ BASE_DIR="$(cd "$(dirname "$0")" && pwd)" # PROXY_AGENT_IMAGE and PROXY_AGENT_REGISTRY_TAGS_URL are overridable so the # test suite can run against a stub image and registry. IMAGE="${PROXY_AGENT_IMAGE:-us-docker.pkg.dev/prod-eng-fivetran-public-repos/public-docker-us/proxy-agent}" +# Appended to the plain agent version to form the Linux image tag. +IMAGE_TAG_SUFFIX="-ubuntu-26.04" CONFIG_FILE="$BASE_DIR/config/config.json" VERSION_FILE="$BASE_DIR/version" SETTINGS_FILE="$BASE_DIR/settings.sh" @@ -39,8 +41,8 @@ if [ ! -f "$VERSION_FILE" ]; then exit 1 fi # Also strips the UTF-8 byte order mark an editor on Windows may add. -CURRENT_VERSION=$(LC_ALL=C tr -d '[:space:]\357\273\277' < "$VERSION_FILE") -if [ -z "$CURRENT_VERSION" ]; then +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 @@ -82,12 +84,14 @@ get_latest_version() { } echo "$tags_json" \ | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+-ubuntu-26\.04"' \ - | tr -d '"' \ + | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' \ | sort -V \ | tail -1 \ || true } +# Prints the plain agent version for a version file entry. Accepts a plain +# version (1.2.9) or a legacy image tag (1.2.9-ubuntu-26.04). get_release_version() { local tag="$1" if [[ "$tag" =~ ^([0-9]+\.[0-9]+\.[0-9]+)(-ubuntu-26\.04)?$ ]]; then @@ -95,10 +99,18 @@ get_release_version() { return 0 fi - echo "ERROR: Invalid Linux proxy-agent image tag '$tag'. Expected or -ubuntu-26.04." >&2 + echo "ERROR: Invalid proxy agent version '$tag' in $VERSION_FILE. Expected ..." >&2 return 1 } +image_tag() { + echo "${IMAGE}:$1${IMAGE_TAG_SUFFIX}" +} + +pin_version() { + echo "$1" > "$VERSION_FILE" +} + upgrade_agent() { echo "Checking for latest version..." local latest_version @@ -107,18 +119,16 @@ upgrade_agent() { echo "ERROR: Unable to determine latest version" >&2 exit 1 fi - local current_release latest_release - current_release=$(get_release_version "$CURRENT_VERSION") || exit 1 - latest_release=$(get_release_version "$latest_version") || exit 1 - if [ "$latest_release" = "$current_release" ]; then + if [ "$latest_version" = "$CURRENT_VERSION" ]; then + pin_version "$CURRENT_VERSION" echo "Already running the latest version ($CURRENT_VERSION)." exit 0 fi log "Upgrading from $CURRENT_VERSION to $latest_version..." - echo "$latest_version" > "$VERSION_FILE" + pin_version "$latest_version" if ! start_agent "$latest_version"; then log "Upgrade failed, rolling back to $CURRENT_VERSION..." - echo "$CURRENT_VERSION" > "$VERSION_FILE" + pin_version "$CURRENT_VERSION" if ! start_agent "$CURRENT_VERSION"; then log "Rollback to $CURRENT_VERSION failed." fi @@ -159,7 +169,7 @@ start_agent() { --health-start-period 30s \ -v "$BASE_DIR/config/config.json:/config/config.json:ro" \ -v "$BASE_DIR/logs:$CONTAINER_LOG_DIR" \ - "${IMAGE}:${version}" \ + "$(image_tag "$version")" \ -i /config/config.json local timeout=60 @@ -193,7 +203,9 @@ start_agent() { case "${1:-}" in start) + CURRENT_VERSION=$(get_release_version "$PINNED_VERSION") || exit 1 check_memory_allocation + pin_version "$CURRENT_VERSION" start_agent "$CURRENT_VERSION" ;; @@ -207,13 +219,16 @@ case "${1:-}" in ;; restart) + CURRENT_VERSION=$(get_release_version "$PINNED_VERSION") || exit 1 check_memory_allocation log "Restarting ${CONTAINER_NAME}..." stop_agent + pin_version "$CURRENT_VERSION" start_agent "$CURRENT_VERSION" ;; upgrade) + CURRENT_VERSION=$(get_release_version "$PINNED_VERSION") || exit 1 check_memory_allocation upgrade_agent ;; diff --git a/tests/bash/manager.bats b/tests/bash/manager.bats index a643401..8fa0bde 100644 --- a/tests/bash/manager.bats +++ b/tests/bash/manager.bats @@ -16,7 +16,7 @@ setup() { mkdir -p "$AGENT_DIR/config" cp "$REPO_ROOT/proxy-agent-manager.sh" "$AGENT_DIR/" echo '{"agent_id": "abc123"}' > "$AGENT_DIR/config/config.json" - echo "1.2.9-ubuntu-26.04" > "$AGENT_DIR/version" + echo "1.2.9" > "$AGENT_DIR/version" # State shared with the stubs in ./stubs; see the comments there. export DOCKER_LOG="$AGENT_DIR/docker.log" @@ -40,11 +40,32 @@ last_image() { cat "$DOCKER_LAST_IMAGE" } +version_file() { + cat "$AGENT_DIR/version" +} + @test "start runs the pinned Ubuntu image with the default memory limit" { run manager start [ "$status" -eq 0 ] [ "$(last_image)" = "$IMAGE_REPO:1.2.9-ubuntu-26.04" ] grep -q -- "--memory=5120m" "$DOCKER_LOG" + [ "$(version_file)" = "1.2.9" ] +} + +@test "start accepts a legacy Ubuntu image tag and rewrites it to the plain version" { + echo "1.2.9-ubuntu-26.04" > "$AGENT_DIR/version" + run manager start + [ "$status" -eq 0 ] + [ "$(last_image)" = "$IMAGE_REPO:1.2.9-ubuntu-26.04" ] + [ "$(version_file)" = "1.2.9" ] +} + +@test "restart rewrites a legacy Ubuntu image tag to the plain version" { + echo "1.2.9-ubuntu-26.04" > "$AGENT_DIR/version" + run manager restart + [ "$status" -eq 0 ] + [ "$(last_image)" = "$IMAGE_REPO:1.2.9-ubuntu-26.04" ] + [ "$(version_file)" = "1.2.9" ] } @test "start rejects an invalid MEMORY_ALLOCATION_MB in settings.sh" { @@ -63,6 +84,24 @@ last_image() { [ ! -f "$DOCKER_LAST_IMAGE" ] } +@test "stop, status, and logs work with a malformed version file" { + manager start + echo "2.0" > "$AGENT_DIR/version" + + run manager status + [ "$status" -eq 0 ] + [[ "$output" == *"NAMES IMAGE STATUS"* ]] + + run manager logs + [ "$status" -eq 0 ] + grep -q "^logs -f proxy-agent-abc123" "$DOCKER_LOG" + + run manager stop + [ "$status" -eq 0 ] + [ ! -f "$DOCKER_LAST_IMAGE" ] + [ "$(version_file)" = "2.0" ] +} + @test "restart and upgrade reject an invalid MEMORY_ALLOCATION_MB without touching the agent" { manager start echo "MEMORY_ALLOCATION_MB=4GB" > "$AGENT_DIR/settings.sh" @@ -118,7 +157,7 @@ last_image() { run manager upgrade [ "$status" -eq 0 ] [ "$(last_image)" = "$IMAGE_REPO:1.2.10-ubuntu-26.04" ] - [[ "$(cat "$AGENT_DIR/version")" == 1.2.10* ]] + [ "$(version_file)" = "1.2.10" ] } @test "upgrade queries the registry tag listing, or its override" { @@ -141,6 +180,15 @@ last_image() { ! grep -q "^run" "$DOCKER_LOG" } +@test "upgrade from a legacy Ubuntu image tag rewrites it to the plain version when already latest" { + export STUB_CURL_RESPONSE='{"tags": ["1.2.9-ubuntu-26.04","1.2.11"]}' + echo "1.2.9-ubuntu-26.04" > "$AGENT_DIR/version" + run manager upgrade + [ "$status" -eq 0 ] + [[ "$output" == *"Already running the latest version (1.2.9)"* ]] + [ "$(version_file)" = "1.2.9" ] +} + @test "upgrade is a no-op when a bare version file entry matches the latest Ubuntu tag" { export STUB_CURL_RESPONSE='{"tags": ["1.2.10-ubuntu-26.04","1.2.11"]}' echo "1.2.10" > "$AGENT_DIR/version" @@ -156,21 +204,29 @@ last_image() { [ "$status" -eq 1 ] [[ "$output" == *"rolling back"* ]] [ "$(last_image)" = "$IMAGE_REPO:1.2.9-ubuntu-26.04" ] - [[ "$(cat "$AGENT_DIR/version")" == 1.2.9* ]] + [ "$(version_file)" = "1.2.9" ] } @test "upgrade reports a failed rollback" { export DOCKER_UNHEALTHY_TAG="1.2." run manager upgrade [ "$status" -eq 1 ] - [[ "$output" == *"Rollback to 1.2.9-ubuntu-26.04 failed"* ]] + [[ "$output" == *"Rollback to 1.2.9 failed"* ]] } @test "upgrade rejects an invalid version file entry" { echo "2.0" > "$AGENT_DIR/version" run manager upgrade [ "$status" -eq 1 ] - [[ "$output" == *"Invalid"* ]] + [[ "$output" == *"Invalid proxy agent version '2.0'"* ]] +} + +@test "start rejects a Windows image tag" { + echo "1.2.9-windows-ltsc2022" > "$AGENT_DIR/version" + run manager start + [ "$status" -eq 1 ] + [[ "$output" == *"Invalid proxy agent version '1.2.9-windows-ltsc2022'"* ]] + [ ! -f "$DOCKER_LAST_IMAGE" ] } @test "stop removes the running container" { diff --git a/tests/integration/linux.sh b/tests/integration/linux.sh index c3d0e24..28728ca 100755 --- a/tests/integration/linux.sh +++ b/tests/integration/linux.sh @@ -60,16 +60,12 @@ assert_healthy() { assert_version_file() { local content content=$(cat "$INSTALL_DIR/version") - [[ "$content" == "$1"* ]] || fail "expected version file to start with $1, got '$content'" + [ "$content" = "$1" ] || fail "expected version file to contain $1, got '$content'" } echo "== Building stub image" docker build -q -f "$REPO_ROOT/tests/stub/Dockerfile.linux" -t "$STUB_IMAGE:$OLD_VERSION$SUFFIX" "$REPO_ROOT/tests/stub" -# The bare tags let the rollback step work whether the management script pins -# the plain version or the full image tag. -for tag in "$OLD_VERSION" "$NEW_VERSION" "$NEW_VERSION$SUFFIX"; do - docker tag "$STUB_IMAGE:$OLD_VERSION$SUFFIX" "$STUB_IMAGE:$tag" -done +docker tag "$STUB_IMAGE:$OLD_VERSION$SUFFIX" "$STUB_IMAGE:$NEW_VERSION$SUFFIX" echo "== Starting stub registry on port $PORT" python3 "$REPO_ROOT/tests/integration/registry_server.py" --port "$PORT" --root "$REPO_ROOT" \ @@ -112,11 +108,15 @@ status_output=$("$MANAGER" status) echo "== Rolling back to $OLD_VERSION via the version file" echo "$OLD_VERSION" > "$INSTALL_DIR/version" "$MANAGER" start -case "$(current_image)" in - "$STUB_IMAGE:$OLD_VERSION"|"$STUB_IMAGE:$OLD_VERSION$SUFFIX") ;; - *) fail "expected rollback to $OLD_VERSION, got $(current_image)" ;; -esac +assert_image "$STUB_IMAGE:$OLD_VERSION$SUFFIX" +assert_healthy + +echo "== Starting from a legacy image tag in the version file" +echo "$OLD_VERSION$SUFFIX" > "$INSTALL_DIR/version" +"$MANAGER" start +assert_image "$STUB_IMAGE:$OLD_VERSION$SUFFIX" assert_healthy +assert_version_file "$OLD_VERSION" echo "== Upgrading" "$MANAGER" upgrade diff --git a/tests/integration/windows.ps1 b/tests/integration/windows.ps1 index 15df7a5..e40bbbb 100644 --- a/tests/integration/windows.ps1 +++ b/tests/integration/windows.ps1 @@ -62,9 +62,9 @@ function Assert-Healthy { } function Assert-VersionFile { - param([string]$Prefix) + param([string]$Expected) $content = (Get-Content -LiteralPath (Join-Path $InstallDir 'version') -Raw).Trim() - if (-not $content.StartsWith($Prefix)) { Fail "expected version file to start with $Prefix, got '$content'" } + if ($content -ne $Expected) { Fail "expected version file to contain $Expected, got '$content'" } } function Invoke-Manager { @@ -131,6 +131,7 @@ try { Assert-VersionFile $NewVersion $settings = Get-Content -LiteralPath (Join-Path $InstallDir 'settings.ps1') -Raw if ($settings -notmatch '\$MEMORY_ALLOCATION_MB = 512') { Fail 'settings.ps1 does not record the memory allocation' } + if ($settings -match 'WINDOWS_VERSION') { Fail 'settings.ps1 records a Windows version although none was given' } $memory = [int64](docker inspect -f '{{.HostConfig.Memory}}' $Container).Trim() if ($memory -ne 512MB) { Fail "container memory limit not applied (got $memory)" } Assert-ConfigMatches $InstallDir 'config.json' @@ -145,6 +146,15 @@ try { Assert-Image "${StubImage}:$oldTag" Assert-Healthy + Write-Host '== Starting from a legacy image tag in the version file' + Set-Content -LiteralPath (Join-Path $InstallDir 'version') -Value $oldTag -Encoding ascii + $null = Invoke-Manager start + Assert-Image "${StubImage}:$oldTag" + Assert-Healthy + Assert-VersionFile $OldVersion + $settings = Get-Content -LiteralPath (Join-Path $InstallDir 'settings.ps1') -Raw + if ($settings -notmatch [regex]::Escape("`$WINDOWS_VERSION = '$ltsc'")) { Fail 'the legacy tag variant was not saved to settings.ps1' } + Write-Host '== Upgrading' $null = Invoke-Manager upgrade Assert-Image "${StubImage}:$newTag" @@ -191,9 +201,12 @@ try { Write-Host '== Installing from a WebSocket config migrates it through the configure endpoint' $wsInstallDir = Join-Path $TempRoot 'fivetran-proxy-agent-websocket' - & (Join-Path $RepoRoot 'install.ps1') (Join-Path $Fixtures 'config-websocket.json') -InstallDir $wsInstallDir + # -WindowsVersion is given here, matching the host, to cover persisting it. + & (Join-Path $RepoRoot 'install.ps1') (Join-Path $Fixtures 'config-websocket.json') -InstallDir $wsInstallDir -WindowsVersion $ltsc if ($LASTEXITCODE -ne 0) { Fail "install.ps1 with a WebSocket config exited with $LASTEXITCODE" } Assert-ConfigMatches $wsInstallDir 'config.json' + $wsSettings = Get-Content -LiteralPath (Join-Path $wsInstallDir 'settings.ps1') -Raw + if ($wsSettings -notmatch [regex]::Escape("`$WINDOWS_VERSION = '$ltsc'")) { Fail 'settings.ps1 does not record -WindowsVersion' } Assert-Image "${StubImage}:$newTag" Assert-Healthy $null = Invoke-Manager stop -Dir $wsInstallDir diff --git a/tests/pwsh/Install.Tests.ps1 b/tests/pwsh/Install.Tests.ps1 index 5f6aec5..fad0a3e 100644 --- a/tests/pwsh/Install.Tests.ps1 +++ b/tests/pwsh/Install.Tests.ps1 @@ -127,7 +127,7 @@ Describe 'install.ps1' { $output = Invoke-Installer (New-InstallArgs) $LASTEXITCODE | Should -Be 0 $output | Should -Match 'Using Windows image variant ltsc2022' - Get-VersionFile | Should -Be '1.2.10-windows-ltsc2022' + Get-VersionFile | Should -Be '1.2.10' $global:ProxyAgentStub.ManagerArgs | Should -Be @('start') } @@ -136,16 +136,19 @@ Describe 'install.ps1' { $global:ProxyAgentStub.Tags += '1.2.8-windows-ltsc2019' $null = Invoke-Installer (New-InstallArgs) $LASTEXITCODE | Should -Be 0 - Get-VersionFile | Should -Be '1.2.8-windows-ltsc2019' + Get-VersionFile | Should -Be '1.2.8' } - It 'uses -WindowsVersion instead of detecting the host, even on an unsupported build' { + It 'normalizes -WindowsVersion and uses it instead of detecting the host, even on an unsupported build' { $global:ProxyAgentStub.OperatingSystem = [pscustomobject]@{ ProductType = 3; BuildNumber = 99999; Caption = 'Windows Server vNext' } $arguments = New-InstallArgs - $arguments.WindowsVersion = 'ltsc2025' - $null = Invoke-Installer $arguments + $arguments.WindowsVersion = 'LTSC2025' + $output = Invoke-Installer $arguments $LASTEXITCODE | Should -Be 0 - Get-VersionFile | Should -Be '1.2.11-windows-ltsc2025' + Get-VersionFile | Should -Be '1.2.11' + $output | Should -Match 'Using Windows image variant ltsc2025' + Get-Content -LiteralPath (Join-Path $script:InstallDir 'settings.ps1') -Raw | + Should -Match ([regex]::Escape("`$WINDOWS_VERSION = 'ltsc2025'")) } It 'fails on an unsupported Windows Server build' { @@ -230,7 +233,7 @@ Describe 'install.ps1' { $output = Invoke-Installer (New-InstallArgs) $LASTEXITCODE | Should -Be 0 $output | Should -Match 'Using Linux container image' - Get-VersionFile | Should -Be '1.2.12-ubuntu-26.04' + Get-VersionFile | Should -Be '1.2.12' $global:ProxyAgentStub.ManagerArgs | Should -Be @('start') } diff --git a/tests/pwsh/Manager.Tests.ps1 b/tests/pwsh/Manager.Tests.ps1 index 005ec0f..4989688 100644 --- a/tests/pwsh/Manager.Tests.ps1 +++ b/tests/pwsh/Manager.Tests.ps1 @@ -27,7 +27,7 @@ BeforeAll { # file and the given version file content, and returns the manager's path. function New-AgentDir { param( - [string]$Version = '1.2.9-ubuntu-26.04', + [string]$Version = '1.2.9', [string]$Settings ) $dir = Join-Path ([System.IO.Path]::GetTempPath()) ("proxy-agent-test-" + [guid]::NewGuid()) @@ -46,6 +46,10 @@ BeforeAll { return (Get-Content -LiteralPath (Join-Path $script:AgentDir 'version') -Raw).Trim() } + function Get-SettingsFile { + return Get-Content -LiteralPath (Join-Path $script:AgentDir 'settings.ps1') -Raw + } + # Runs the manager, capturing every output stream as one string. The width # keeps Windows PowerShell 5.1 from wrapping long lines, such as paths, at # the console width. @@ -146,6 +150,15 @@ Describe 'proxy-agent-manager.ps1' { $LASTEXITCODE | Should -Be 0 $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.9-ubuntu-26.04" Get-DockerRunArgs | Should -Contain '--memory=5120m' + Get-VersionFile | Should -Be '1.2.9' + } + + It 'accepts a legacy Ubuntu image tag and rewrites it to the plain version' { + $manager = New-AgentDir -Version '1.2.9-ubuntu-26.04' + $null = Invoke-Manager $manager start + $LASTEXITCODE | Should -Be 0 + $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.9-ubuntu-26.04" + Get-VersionFile | Should -Be '1.2.9' } It 'rejects MEMORY_ALLOCATION_MB = in settings.ps1' -ForEach @( @@ -168,6 +181,25 @@ Describe 'proxy-agent-manager.ps1' { $global:ProxyAgentStub.LastImage | Should -BeNullOrEmpty } + It 'runs <_> with a malformed version file' -ForEach @('stop', 'status', 'logs') { + $manager = New-AgentDir + $null = Invoke-Manager $manager start + Set-Content -LiteralPath (Join-Path $script:AgentDir 'version') -Value '2.0' -Encoding ascii + + $null = Invoke-Manager $manager $_ + + $LASTEXITCODE | Should -Be 0 + Get-VersionFile | Should -Be '2.0' + if ($_ -eq 'stop') { + $global:ProxyAgentStub.LastImage | Should -BeNullOrEmpty + Get-DockerCall 'rm' | Should -Contain 'proxy-agent-abc123' + } elseif ($_ -eq 'status') { + Get-DockerCall 'ps' | Should -Contain 'name=proxy-agent-abc123' + } else { + Get-DockerCall 'logs' | Should -Contain '-f' + } + } + It 'rejects an invalid MEMORY_ALLOCATION_MB on <_> without touching the agent' -ForEach @('restart', 'upgrade') { $manager = New-AgentDir $null = Invoke-Manager $manager start @@ -221,6 +253,7 @@ Describe 'proxy-agent-manager.ps1' { It 'refuses a Windows image tag' { $manager = New-AgentDir -Version '1.2.9-windows-ltsc2022' { & $manager start } | Should -Throw '*Windows containers*' + Get-VersionFile | Should -Be '1.2.9-windows-ltsc2022' } It 'upgrades to the newest Ubuntu tag and ignores bare and Windows tags' { @@ -228,7 +261,7 @@ Describe 'proxy-agent-manager.ps1' { $null = Invoke-Manager $manager upgrade $LASTEXITCODE | Should -Be 0 $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.10-ubuntu-26.04" - Get-VersionFile | Should -BeLike '1.2.10*' + Get-VersionFile | Should -Be '1.2.10' } It 'queries the registry tag listing, or its override, on upgrade' { @@ -251,6 +284,14 @@ Describe 'proxy-agent-manager.ps1' { Get-DockerRunArgs | Should -BeNullOrEmpty } + It 'rewrites a legacy Ubuntu image tag to the plain version on upgrade when already latest' { + $global:ProxyAgentStub.Tags = @('1.2.9-ubuntu-26.04', '1.2.11') + $manager = New-AgentDir -Version '1.2.9-ubuntu-26.04' + $output = Invoke-Manager $manager upgrade + $output | Should -Match ([regex]::Escape('Already running the latest version (1.2.9)')) + Get-VersionFile | Should -Be '1.2.9' + } + It 'rolls back and fails when the upgraded container is unhealthy' { $global:ProxyAgentStub.UnhealthyTag = '1.2.10' $manager = New-AgentDir @@ -258,12 +299,12 @@ Describe 'proxy-agent-manager.ps1' { $LASTEXITCODE | Should -Be 1 $output | Should -Match 'rolling back' $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.9-ubuntu-26.04" - Get-VersionFile | Should -BeLike '1.2.9*' + Get-VersionFile | Should -Be '1.2.9' } It 'rejects an invalid version file entry on upgrade' { $manager = New-AgentDir -Version '2.0' - { & $manager upgrade } | Should -Throw '*Invalid*' + { & $manager upgrade } | Should -Throw "*Invalid proxy agent version '2.0'*" } It 'removes the running container on stop' { @@ -286,17 +327,57 @@ Describe 'proxy-agent-manager.ps1' { $null = Invoke-Manager $manager start $LASTEXITCODE | Should -Be 0 $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.9-windows-ltsc2022" + Get-VersionFile | Should -Be '1.2.9' } - It 'starts a pinned Windows image tag as is' { + It 'starts the LTSC version from the WINDOWS_VERSION setting over the detected one' { + $manager = New-AgentDir -Settings "`$WINDOWS_VERSION = 'ltsc2025'" + $null = Invoke-Manager $manager start + $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.9-windows-ltsc2025" + } + + It 'migrates a legacy Windows image tag to the plain version and the WINDOWS_VERSION setting' { + $manager = New-AgentDir -Version '1.2.9-windows-ltsc2025' -Settings '$MEMORY_ALLOCATION_MB = 2048' + $null = Invoke-Manager $manager start + $LASTEXITCODE | Should -Be 0 + $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.9-windows-ltsc2025" + Get-VersionFile | Should -Be '1.2.9' + $settings = Get-SettingsFile + $settings | Should -Match ([regex]::Escape("`$WINDOWS_VERSION = 'ltsc2025'")) + $settings | Should -Match ([regex]::Escape('$MEMORY_ALLOCATION_MB = 2048')) + + # The migrated variant still wins over the detected ltsc2022. + $null = Invoke-Manager $manager restart + $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.9-windows-ltsc2025" + } + + It 'migrates a legacy Windows image tag when no settings file exists' { + $manager = New-AgentDir -Version '1.2.9-windows-ltsc2025' + $null = Invoke-Manager $manager start + $LASTEXITCODE | Should -Be 0 + Get-SettingsFile | Should -Match ([regex]::Escape("`$WINDOWS_VERSION = 'ltsc2025'")) + } + + It 'migrates a legacy Windows image tag when settings.ps1 has no final newline' { $manager = New-AgentDir -Version '1.2.9-windows-ltsc2025' + [System.IO.File]::WriteAllText((Join-Path $script:AgentDir 'settings.ps1'), '$MEMORY_ALLOCATION_MB = 2048') $null = Invoke-Manager $manager start + $LASTEXITCODE | Should -Be 0 + Get-SettingsFile | Should -Match '2048\r?\n\$WINDOWS_VERSION' + Get-VersionFile | Should -Be '1.2.9' + + $null = Invoke-Manager $manager restart + $LASTEXITCODE | Should -Be 0 $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.9-windows-ltsc2025" } - It 'refuses an Ubuntu image tag' { + It 'treats a legacy Ubuntu image tag as its plain version, with a warning' { $manager = New-AgentDir -Version '1.2.9-ubuntu-26.04' - { & $manager start } | Should -Throw '*Linux containers*' + $output = Invoke-Manager $manager start + $LASTEXITCODE | Should -Be 0 + $output | Should -Match 'WARNING: .*Linux image tag 1\.2\.9-ubuntu-26\.04.*Windows containers' + $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.9-windows-ltsc2022" + Get-VersionFile | Should -Be '1.2.9' } It 'upgrades to the newest tag for the same LTSC version only' { @@ -304,7 +385,15 @@ Describe 'proxy-agent-manager.ps1' { $null = Invoke-Manager $manager upgrade $LASTEXITCODE | Should -Be 0 $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.10-windows-ltsc2022" - Get-VersionFile | Should -BeLike '1.2.10*' + Get-VersionFile | Should -Be '1.2.10' + } + + It 'upgrades within the LTSC version from the WINDOWS_VERSION setting' { + $manager = New-AgentDir -Settings "`$WINDOWS_VERSION = 'ltsc2025'" + $null = Invoke-Manager $manager upgrade + $LASTEXITCODE | Should -Be 0 + $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.10-windows-ltsc2025" + Get-VersionFile | Should -Be '1.2.10' } It 'rolls back and fails when the upgraded container is unhealthy' { @@ -314,7 +403,7 @@ Describe 'proxy-agent-manager.ps1' { $LASTEXITCODE | Should -Be 1 $output | Should -Match 'rolling back' $global:ProxyAgentStub.LastImage | Should -Be "$($script:ImageRepo):1.2.9-windows-ltsc2022" - Get-VersionFile | Should -Be '1.2.9-windows-ltsc2022' + Get-VersionFile | Should -Be '1.2.9' } It 'fails on upgrade when no tags are published for the LTSC version' {