H5: Remove Linux upgrade strategy's cold-cache historical default
Summary
H5 of the 1.8.0 Upgrade Hardening initiative (H1-H4 already merged).
Closes the last gap in the 1.7.x operator failure chain by making LinuxTentacleUpgradeStrategy.CanHandle strict-Linux-only AND adding a symmetric cold-cache guard to MachineUpgradeService.UpgradeAsync so direct API callers can't bypass the FE's H1 upgrade-info gate.
The bug class H5 closes
Pre-H5 LinuxTentacleUpgradeStrategy.CanHandle had two backward-compat "historical default" branches:
if (capabilities == null) return true; // ← null = claim
return capabilities.IsLinux || capabilities.IsUnknown; // ← Unknown = claim
The IsUnknown claim was the root cause of the operator's 1.7.x failure chain:
- Cold-cache Windows machine triggers
GetUpgradeInfoAsync -
ResolveStrategyran withcapabilities.Os = ""(cold cache) - Linux strategy claimed it via
IsUnknown=truehistorical default - Version registry queried Docker Hub for a Linux tarball that doesn't exist for win-x64
- Operator saw
"Docker Hub unreachable, set SQUID_TARGET_LINUX_TENTACLE_VERSION"— and went down the wrong remediation path because they were on Windows
H1 already short-circuits cold-cache tentacle styles at the eligibility evaluator with NoOsDetected, so the FE's GetUpgradeInfo call no longer reaches strategy resolution in the cold case. H5 closes the remaining gap (direct API callers / curl) AND removes the now-dead historical-default code path.
Behaviour changes
| Scenario | Pre-H5 | Post-H5 |
|---|---|---|
LinuxTentacleUpgradeStrategy.CanHandle("TentaclePolling", null) |
true |
false |
LinuxTentacleUpgradeStrategy.CanHandle("TentaclePolling", {Os=""}) |
true |
false |
LinuxTentacleUpgradeStrategy.CanHandle("TentaclePolling", {Os="Unknown"}) |
true |
false |
LinuxTentacleUpgradeStrategy.CanHandle("TentaclePolling", {Os="Linux"}) |
true |
true (unchanged) |
Direct POST /upgrade with cold-cache tentacle machine |
Dispatched via Linux historical default | Rejected with OS not yet detected, run health check (mirrors H1) |
Cross-OS uniformity
The Windows and Linux strategies are now structurally symmetric:
// Windows:
if (capabilities == null) return false;
return capabilities.IsWindows;
// Linux (post-H5):
if (capabilities == null) return false;
return capabilities.IsLinux;
A future MacOSTentacleUpgradeStrategy / BSDTentacleUpgradeStrategy plugs in via Autofac DI without modifying either existing strategy. Open-closed property.
Test plan
-
+2 invariant pins on Linux strategy (empty Os rejected, explicit "Unknown" rejected) -
+1 invariant on UpgradeAsynccold-cache guard (rejected at entry with health-check guidance) -
Repurposed UpgradeAsync_ColdCacheNoOs_RoutesToLinuxAsHistoricalDefault→*_RejectedAtEntry_PointsToHealthCheckwith inverted expectations -
ArrangeMachinehelper auto-seedsos=Linuxso tests that don't care about OS routing continue to exercise the happy path -
5563/5563 unit tests green (+2 net new)
Backward compatibility
-
Operator-facing break: cold-cache + direct
POST /upgradepreviously succeeded (via Linux historical default). It now fails withOS not yet detected. This is the intentional security/correctness improvement — operators runningcurl -X POSTagainst a freshly-registered machine should hit the same guidance the UI gives. - All existing tests adjusted to reflect the new strict-OS contract.
What's NOT in this PR
- H6: Agent rollback + abandonment recovery (next)
- H7: Role/feature capability slots
- H8: Comprehensive E2E test matrix