From 137161801aab4921ba7d45a92329465f0839a194 Mon Sep 17 00:00:00 2001 From: Brett Kinny Date: Tue, 4 Aug 2026 18:33:26 +1000 Subject: [PATCH] Retain shared Candidate images during uninstall --- tests/test-lifecycle-ownership.sh | 13 +++++++++++-- tests/test-lifecycle-static.sh | 2 ++ uninstall.ps1 | 19 +++++++++++-------- uninstall.sh | 28 +++++++++++++++------------- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/tests/test-lifecycle-ownership.sh b/tests/test-lifecycle-ownership.sh index a3f88be..8fec5f3 100755 --- a/tests/test-lifecycle-ownership.sh +++ b/tests/test-lifecycle-ownership.sh @@ -80,7 +80,13 @@ case "$1" in rm) ;; *) exit 2 ;; esac ;; - rm|rmi) ;; + rm) ;; + rmi) + if [ "${FAIL_SHARED_RELEASE_REMOVE:-0}" = 1 ] \ + && [ "$2" = 'ghcr.io/squarewavesystems/squarebox@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' ]; then + echo 'shared release image is in use' >&2 + exit 42 + fi ;; *) exit 2 ;; esac EOF @@ -192,7 +198,8 @@ fi grep -q -- '--force is required' "$TMP/force.out" test -d "$TMP/custom" -"$ROOT/uninstall.sh" --purge --yes --force +export FAIL_SHARED_RELEASE_REMOVE=1 +"$ROOT/uninstall.sh" --purge --yes --force >"$TMP/purge.out" test ! -e "$TMP/custom" test -f "$TMP/home/squarebox/DO-NOT-DELETE" test -f "$TMP/external-workspace/project.txt" @@ -200,6 +207,8 @@ test ! -e "$TMP/home/.squarebox-shell-init" ! grep -qF '# >>> squarebox >>>' "$TMP/home/.bashrc" grep -q '^rm -f squarebox$' "$TMP/runtime.log" grep -q '^rmi squarebox$' "$TMP/runtime.log" +! grep -q '^rmi ghcr.io/squarewavesystems/squarebox@sha256:' "$TMP/runtime.log" +grep -q 'Retaining shared Candidate image ref' "$TMP/purge.out" grep -q '^volume rm custom-home$' "$TMP/runtime.log" echo 'ok - lifecycle deletion requires identity, reachability, adoption force, and recorded paths' diff --git a/tests/test-lifecycle-static.sh b/tests/test-lifecycle-static.sh index 0abbf60..a1ce98e 100755 --- a/tests/test-lifecycle-static.sh +++ b/tests/test-lifecycle-static.sh @@ -72,6 +72,8 @@ grep -q '\$HomeVolume -cne \$State.HOME_VOLUME' install.ps1 grep -q '\$owner.Trim() -cne '\''__INSTALL_ID__' install.ps1 grep -q '\$script:RollbackArmed' install.ps1 grep -q 'changed ownership after confirmation' uninstall.ps1 +grep -q 'Retaining shared Candidate image ref' uninstall.sh +grep -q 'Retaining shared Candidate image ref' uninstall.ps1 ! grep -Eq '\$owner([.]Trim[(][)])?[[:space:]]+-ne[[:space:]]+\$InstallId' install.ps1 uninstall.ps1 ! grep -Eq 'sudo[[:space:]]+rm[[:space:]]+-rf' uninstall.sh for file in install.sh uninstall.sh; do diff --git a/uninstall.ps1 b/uninstall.ps1 index e6fc3ba..8e423ca 100644 --- a/uninstall.ps1 +++ b/uninstall.ps1 @@ -186,6 +186,7 @@ $ContainerName = if ($State) { $State.CONTAINER_NAME } else { 'squarebox' } $ImageAlias = if ($State) { $State.IMAGE_ALIAS } else { 'squarebox' } $ImageRef = if ($State) { $State.IMAGE_REF } else { 'squarebox' } $ImageId = if ($State) { $State.IMAGE_ID } else { '' } +$SharedImageRef = $State -and $State.BUILD -ceq '0' -and $ImageRef -cne $ImageAlias $HomeVolume = if ($State) { $State.HOME_VOLUME } elseif ($env:SQUAREBOX_HOME_VOLUME) { $env:SQUAREBOX_HOME_VOLUME } else { 'squarebox-home' } $HomeVolumeAdopted = $State -and $State.HOME_VOLUME_ADOPTED -eq '1' $ProfilePath = if ($State -and $State.SHELL_RC) { $State.SHELL_RC } else { $PROFILE.CurrentUserAllHosts } @@ -280,7 +281,7 @@ Write-Host '' Write-Host 'Will remove:' $Anything = $false if ($ContainerOwned) { Write-Host " - Managed Box: $ContainerName"; $Anything = $true } -if ($ImageOwned) { Write-Host " - Recorded image refs: $ImageAlias and $ImageRef"; $Anything = $true } +if ($ImageOwned) { Write-Host " - Recorded image alias: $ImageAlias"; $Anything = $true } if ($HasProfile) { Write-Host " - PowerShell adapter(s): $($ProfilePaths -join ', ')"; $Anything = $true } if ($Purge -and (Test-Path $InstallDir)) { Write-Host " - Recorded install directory: $InstallDir"; $Anything = $true } if ($Purge -and $VolumeOwned) { Write-Host " - Managed home: $HomeVolume"; $Anything = $true } @@ -323,14 +324,16 @@ if ($ContainerOwned) { if ($LASTEXITCODE -ne 0) { Abort "Failed to remove managed Box '$ContainerName'." } } if ($ImageOwned) { - foreach ($ref in @($ImageAlias, $ImageRef) | Select-Object -Unique) { - & $Runtime image inspect $ref 2>$null | Out-Null - if ($LASTEXITCODE -ne 0) { continue } - $refId = (& $Runtime image inspect -f '{{.Id}}' $ref).Trim() - if ($State -and $refId -cne $ImageId) { Abort "Image ref '$ref' changed ownership during uninstall." } - & $Runtime rmi $ref | Out-Null - if ($LASTEXITCODE -ne 0) { Abort "Image ref '$ref' is still in use or could not be removed." } + & $Runtime image inspect $ImageAlias 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + $refId = (& $Runtime image inspect -f '{{.Id}}' $ImageAlias).Trim() + if ($State -and $refId -cne $ImageId) { Abort "Image alias '$ImageAlias' changed ownership during uninstall." } + & $Runtime rmi $ImageAlias | Out-Null + if ($LASTEXITCODE -ne 0) { Abort "Image alias '$ImageAlias' is still in use or could not be removed." } } + # Published refs identify shared Candidate cache, not a resource owned by + # this Install identity. Another Box may still depend on the same digest. + if ($SharedImageRef) { Write-Host "Retaining shared Candidate image ref $ImageRef." } } if ($HasProfile) { foreach ($path in $ProfileBlocks) { Remove-SquareboxProfileBlock $path } diff --git a/uninstall.sh b/uninstall.sh index 9a1c363..097d651 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -359,7 +359,7 @@ echo "" echo "Will remove:" anything=0 [ "$container_owned" = 1 ] && { echo " - Managed Box: $CONTAINER_NAME"; anything=1; } -[ "$image_owned" = 1 ] && { echo " - Recorded image refs: $IMAGE_ALIAS ${IMAGE_REF:+and $IMAGE_REF}"; anything=1; } +[ "$image_owned" = 1 ] && { echo " - Recorded image alias: $IMAGE_ALIAS"; anything=1; } [ "$has_shell_init" = 1 ] && { echo " - Shell adapter: $SHELL_INIT"; anything=1; } for _rc in "${rc_files[@]}"; do echo " - Shell sentinel: $_rc"; anything=1; done [ "$has_bridge" = 1 ] && { echo " - Git Bash bridge: $HOME/.bash_profile"; anything=1; } @@ -411,19 +411,21 @@ if [ "$container_owned" = 1 ]; then fi if [ "$image_owned" = 1 ]; then - echo "Removing recorded image references..." - _refs=("$IMAGE_ALIAS"); [ -n "$IMAGE_REF" ] && [ "$IMAGE_REF" != "$IMAGE_ALIAS" ] && _refs+=("$IMAGE_REF") - for _ref in "${_refs[@]}"; do - if rt_cmd image inspect "$_ref" >/dev/null 2>&1; then - _ref_id="$(rt_cmd image inspect -f '{{.Id}}' "$_ref")" - if [ "$HAD_STATE" = 1 ] && [ "$_ref_id" != "$IMAGE_ID" ]; then - echo "Error: image ref '$_ref' changed ownership during uninstall; refusing removal." >&2; exit 1 - fi - rt_cmd rmi "$_ref" >/dev/null || { - echo "Error: image ref '$_ref' is still in use or could not be removed." >&2; exit 1; - } + echo "Removing recorded image alias..." + if rt_cmd image inspect "$IMAGE_ALIAS" >/dev/null 2>&1; then + _ref_id="$(rt_cmd image inspect -f '{{.Id}}' "$IMAGE_ALIAS")" + if [ "$HAD_STATE" = 1 ] && [ "$_ref_id" != "$IMAGE_ID" ]; then + echo "Error: image alias '$IMAGE_ALIAS' changed ownership during uninstall; refusing removal." >&2; exit 1 fi - done + rt_cmd rmi "$IMAGE_ALIAS" >/dev/null || { + echo "Error: image alias '$IMAGE_ALIAS' is still in use or could not be removed." >&2; exit 1; + } + fi + # Published refs identify shared Candidate cache, not a resource owned by + # this Install identity. Another Box may still depend on the same digest. + if [ "$HAD_STATE" = 1 ] && [ "$BUILD" = 0 ] && [ -n "$IMAGE_REF" ] && [ "$IMAGE_REF" != "$IMAGE_ALIAS" ]; then + echo "Retaining shared Candidate image ref $IMAGE_REF." + fi fi rm -f "$SHELL_INIT"