Skip to content

Add handler-level static capability requirements (slot-based, AND/OR)

Summary

  • New property IActionHandler.StaticRequirements (default empty) lets handlers declare plan-time capability requirements as a map of slot → acceptable values. Match semantics: AND across slots (every declared slot must be satisfied), OR within a slot (handler accepts any value in the acceptable-values set).
  • The single mechanism handles OS, shell, binary, privilege, architecture, and any future dimension expressible as namespaced strings — no interface changes needed when a new capability category appears.
  • CapabilityValidator.ValidateStaticRequirements is the new validator dimension. The planner pulls the handler from the registry, projects the target's MachineRuntimeCapabilities into the slot-map shape via MachineCapabilitySet, and matches slot-by-slot. Violations flow through the existing CapabilityViolation pipeline → PlanBlockingReasonCodes.CapabilityViolation → preview UI.
  • IISDeployActionHandler declares {os: {windows}, shell:powershell: {present}}. RunScriptActionHandler declares {os: {windows, linux, macos}} — the explicit OR-within-slot example. Existing dispatch-time per-handler guards stay in place as the runtime safety net.

Design choices

Slot-based, not boolean-per-dimension — IReadOnlyDictionary<string, IReadOnlySet<string>> keeps the surface generic. Adding a new capability dimension (e.g. min agent version, network egress) doesn't require interface changes; just add a new namespaced slot (agent-min-version:1.7.0, net:outbound-https).

AND across slots, OR within a slot — covers the realistic distribution of action requirements: IIS deploy needs Windows AND PowerShell (two slots); RunScript needs Windows OR Linux OR macOS (one slot, three values); Helm needs Linux OR macOS AND bin:helm AND bin:kubectl (three slots, one with two values).

Strict slot semantics at plan time — a slot the handler requires that the target hasn't advertised IS a violation. The message tells the operator to run a health check on the target. The dispatch-time per-handler guard (e.g. IISDeployActionHandler.EnsureWindowsTentacleTarget) keeps the optimistic-allow safety net for cache-went-stale-between-preview-and-execute.

OS-string tolerance in projection — MachineCapabilitySet.From normalises both the canonical "Windows" (current Tentacle) AND the legacy long form "Microsoft Windows NT 10.0.19045.0" (older binaries) into os: windows. Anchored on StartsWith("Microsoft Windows"), not Contains("Windows"), with explicit anti-false-positive tests (LinuxOnWindowsSubsystem must NOT match).

Test plan

  • dotnet build Squid.sln → 0 errors
  • dotnet test --filter "Capability" (new tests) → 66/66 pass, 186ms
  • dotnet test (full Squid.UnitTests) → 5410/5410 pass, zero regression
  • CI: unit + integration tests on the workflow
  • Manual smoke (post-merge): create release for an IIS step against a target whose cache reports a non-Windows OS — preview should now show a CapabilityViolation with the slot-mismatch detail BEFORE the operator clicks Deploy

Future capability dimensions (no interface changes needed)

The same mechanism naturally extends to:

  • Binary presence: bin:kubectl, bin:helm, bin:docker — Helm action declares bin:helm: {present}
  • Privilege: priv:admin, priv:sudo — WindowsServiceInstall action declares priv:admin: {present}
  • Architecture: arch:x64, arch:arm64 — handlers needing native binaries declare matching arch
  • Comparison-based (e.g. min agent version) would need a separate dimension since flat strings can't express ordering; YAGNI for now

Backward compatibility

  • IActionHandler.StaticRequirements defaults to empty → every existing handler keeps its current behaviour
  • CapabilityValidator.Validate (existing 5-dimension method) is unchanged → existing planner check paths untouched
  • DeploymentPlanner constructor adds one new dependency (IMachineRuntimeCapabilitiesCache); existing planner test fixtures updated with a permissive default

Merge request reports

Loading