190 lines
5.2 KiB
Go
190 lines
5.2 KiB
Go
package fleet
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// TierType identifies one of 14 Linux deploy mechanisms.
|
|
type TierType string
|
|
|
|
const (
|
|
TierSSHKey TierType = "ssh_key"
|
|
TierCurlBash TierType = "curl_bash"
|
|
TierAnsiblePull TierType = "ansible_pull"
|
|
TierPodmanRootless TierType = "podman_rootless"
|
|
TierSystemdTransient TierType = "systemd_transient"
|
|
TierSnapFlatpak TierType = "snap_flatpak"
|
|
TierLANCachePeer TierType = "lan_cache_peer"
|
|
TierDNSTXT TierType = "dns_txt"
|
|
TierMTLSWireGuard TierType = "mtls_wireguard"
|
|
TierImmutableOCI TierType = "immutable_oci"
|
|
TierNixFlake TierType = "nix_flake"
|
|
TierErasureReasm TierType = "erasure_reassembly"
|
|
TierFleetTorrent TierType = "fleet_torrent"
|
|
TierOfflineBundle TierType = "offline_contingency"
|
|
)
|
|
|
|
// DefaultTierOrder returns the canonical 14-tier Linux deploy sequence.
|
|
func DefaultTierOrder() []TierType {
|
|
return []TierType{
|
|
TierSSHKey,
|
|
TierCurlBash,
|
|
TierAnsiblePull,
|
|
TierPodmanRootless,
|
|
TierSystemdTransient,
|
|
TierSnapFlatpak,
|
|
TierLANCachePeer,
|
|
TierDNSTXT,
|
|
TierMTLSWireGuard,
|
|
TierImmutableOCI,
|
|
TierNixFlake,
|
|
TierErasureReasm,
|
|
TierFleetTorrent,
|
|
TierOfflineBundle,
|
|
}
|
|
}
|
|
|
|
// TierSlot maps 1-based tier index to type.
|
|
func TierSlot(n int) (TierType, error) {
|
|
order := DefaultTierOrder()
|
|
if n < 1 || n > len(order) {
|
|
return "", fmt.Errorf("tier %d out of range 1-%d", n, len(order))
|
|
}
|
|
return order[n-1], nil
|
|
}
|
|
|
|
// TierExecutor runs deploy phases for a single tier.
|
|
type TierExecutor struct {
|
|
Store *Store
|
|
HostID string
|
|
Report *ReconReport
|
|
}
|
|
|
|
// TierResult captures outcome of a tier attempt.
|
|
type TierResult struct {
|
|
Tier int `json:"tier"`
|
|
Type TierType `json:"type"`
|
|
Phase string `json:"phase"`
|
|
Status string `json:"status"`
|
|
Error string `json:"error,omitempty"`
|
|
Elapsed time.Duration `json:"elapsed_ms"`
|
|
}
|
|
|
|
// ExecuteTier runs recon → patch_first gate → deploy for one tier slot.
|
|
func (e *TierExecutor) ExecuteTier(ctx context.Context, tierNum int) (*TierResult, error) {
|
|
tierType, err := TierSlot(tierNum)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
start := time.Now()
|
|
result := &TierResult{Tier: tierNum, Type: tierType, Phase: "recon", Status: "running"}
|
|
|
|
if e.Store != nil && e.Report != nil {
|
|
phenotype := PhenotypeFromRecon(e.Report)
|
|
if skip, _ := e.Store.ShouldSkipTier(ctx, phenotype, tierNum); skip {
|
|
result.Phase = "atlas_skip"
|
|
result.Status = "skipped"
|
|
_ = e.Store.LogLOTL(ctx, e.HostID, tierNum, "atlas_skip", "skipped", "failure atlas immune", ReconJSON(e.Report))
|
|
return result, nil
|
|
}
|
|
}
|
|
|
|
// Recon phase (read-only, already done at executor init)
|
|
_ = e.Store.LogLOTL(ctx, e.HostID, tierNum, "recon", "success", "", ReconJSON(e.Report))
|
|
|
|
// Patch-first gate
|
|
result.Phase = "patch_first"
|
|
if !e.passPatchGate(tierType) {
|
|
result.Status = "skipped"
|
|
result.Error = "patch_first gate failed"
|
|
_ = e.Store.LogLOTL(ctx, e.HostID, tierNum, "patch_first", "skipped", result.Error, "")
|
|
_ = e.Store.RecordFailure(ctx, PhenotypeFromRecon(e.Report), tierNum)
|
|
return result, nil
|
|
}
|
|
_ = e.Store.LogLOTL(ctx, e.HostID, tierNum, "patch_first", "success", "", "")
|
|
|
|
// Deploy phase (simulated success paths per tier capability)
|
|
result.Phase = "deploy"
|
|
deployErr := e.deploy(ctx, tierType)
|
|
if deployErr != nil {
|
|
result.Status = "failed"
|
|
result.Error = deployErr.Error()
|
|
_ = e.Store.LogLOTL(ctx, e.HostID, tierNum, "deploy", "failed", result.Error, "")
|
|
_ = e.Store.RecordFailure(ctx, PhenotypeFromRecon(e.Report), tierNum)
|
|
} else {
|
|
result.Status = "success"
|
|
_ = e.Store.LogLOTL(ctx, e.HostID, tierNum, "deploy", "success", "", "")
|
|
_ = e.Store.RecordWin(ctx, PhenotypeFromRecon(e.Report), tierNum)
|
|
}
|
|
|
|
result.Elapsed = time.Since(start)
|
|
return result, nil
|
|
}
|
|
|
|
func (e *TierExecutor) passPatchGate(t TierType) bool {
|
|
if e.Report == nil {
|
|
return true
|
|
}
|
|
switch t {
|
|
case TierPodmanRootless, TierImmutableOCI:
|
|
return e.Report.PodmanAvailable || e.Report.DockerAvailable
|
|
case TierNixFlake:
|
|
return commandExists("nix")
|
|
case TierMTLSWireGuard:
|
|
return commandExists("wg")
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (e *TierExecutor) deploy(ctx context.Context, t TierType) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
switch t {
|
|
case TierPodmanRootless:
|
|
if e.Report != nil && !e.Report.PodmanAvailable {
|
|
return fmt.Errorf("podman not available")
|
|
}
|
|
case TierImmutableOCI:
|
|
if e.Report == nil || (!e.Report.PodmanAvailable && !e.Report.DockerAvailable) {
|
|
return fmt.Errorf("no container runtime")
|
|
}
|
|
case TierNixFlake:
|
|
if !commandExists("nix") {
|
|
return fmt.Errorf("nix not installed")
|
|
}
|
|
}
|
|
// Authorized deploy tiers succeed when gates pass; actual spawn is agent-side.
|
|
return nil
|
|
}
|
|
|
|
// RunTierChain executes tiers in order until one succeeds or all fail.
|
|
func RunTierChain(ctx context.Context, store *Store, hostID string, order []int) ([]*TierResult, error) {
|
|
report, err := RunRecon()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exec := &TierExecutor{Store: store, HostID: hostID, Report: report}
|
|
var results []*TierResult
|
|
|
|
for _, tierNum := range order {
|
|
res, err := exec.ExecuteTier(ctx, tierNum)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
results = append(results, res)
|
|
if res.Status == "success" {
|
|
break
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|