Files
LINUX-AETHERFORGE/internal/fleet/tiers.go
drjones 3678b199d0
Some checks failed
Test / test (push) Has been cancelled
Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
2026-07-04 09:31:23 +00:00

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
}