Files
agent-lib/internal/client/executor.go
T

164 lines
4.4 KiB
Go
Raw Normal View History

package client
import (
"fmt"
"io"
"os"
"time"
"github.com/m3tam3re/agent-lib/internal/deploy"
"github.com/m3tam3re/agent-lib/internal/gitsource"
"github.com/m3tam3re/agent-lib/internal/lockfile"
"github.com/m3tam3re/agent-lib/internal/target"
)
// SyncReport summarizes one planner-driven sync run.
type SyncReport struct {
Revision string
Deployed int
Updated int
Kept int
Removed int
Skipped int
Left int
Manifest string
}
// ExecutePlan applies a plan item by item: deploys and removes are atomic
// per item, every skip+warn and leave+warn is appended to the warning log
// with item name and reason, and the manifest is rewritten once at the end.
func ExecutePlan(tree *gitsource.GitTree, plan []PlanEntry, manifest *Manifest, paths StatePaths, rev string, stdout io.Writer) (*SyncReport, error) {
report := &SyncReport{Revision: rev, Manifest: paths.Manifest}
var warnings []string
for _, e := range plan {
key := manifestKey(e.Type, e.Name)
switch e.Action {
case ActDeploy, ActUpdate:
if err := deployEntry(tree, e); err != nil {
return nil, fmt.Errorf("%s %s/%s: %w", e.Action, e.Type, e.Name, err)
}
manifest.Items[key] = &ManifestItem{
Name: e.Name, Type: e.Type, Origin: e.Origin, Revision: rev, Hash: e.NewHash,
}
if e.Action == ActDeploy {
report.Deployed++
} else {
report.Updated++
}
case ActKeep:
report.Kept++
case ActRemove:
if err := os.RemoveAll(e.DstPath); err != nil {
return nil, fmt.Errorf("removing %s/%s: %w", e.Type, e.Name, err)
}
delete(manifest.Items, key)
report.Removed++
case ActSkip:
warnings = append(warnings, fmt.Sprintf("SKIP %s/%s: %s", e.Type, e.Name, e.Reason))
report.Skipped++
case ActLeave:
warnings = append(warnings, fmt.Sprintf("LEAVE %s/%s: %s", e.Type, e.Name, e.Reason))
report.Left++
}
}
manifest.Version = ManifestVersion
manifest.Revision = rev
if err := manifest.Save(paths.Manifest); err != nil {
return nil, err
}
if len(warnings) > 0 {
if err := appendWarnings(paths.LogFile, warnings); err != nil {
fmt.Fprintf(stdout, "warning: could not write sync log: %v\n", err)
}
}
printPlannedSyncReport(stdout, report)
return report, nil
}
func deployEntry(tree *gitsource.GitTree, e PlanEntry) error {
if e.Type == lockfile.TypeSkill {
return deploy.Dir(tree, e.RelPath, e.DstPath)
}
return deploy.File(tree, e.RelPath, e.DstPath)
}
// appendWarnings adds timestamped warning lines to the sync log.
func appendWarnings(logPath string, warnings []string) error {
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
stamp := time.Now().UTC().Format(time.RFC3339)
for _, w := range warnings {
if _, err := fmt.Fprintf(f, "%s %s\n", stamp, w); err != nil {
return err
}
}
return nil
}
// diskFacts observes every deployed path the planner can care about:
// manifest items, current inventory items, and unmanaged occupants of the
// target directories.
func diskFacts(oc *target.OpenCode, manifest *Manifest, items []PlannedItem) map[string]DiskFact {
facts := map[string]DiskFact{}
observe := func(typ, name string) {
key := manifestKey(typ, name)
if _, seen := facts[key]; seen {
return
}
path, ok := oc.DeployPath(typ, name)
if !ok {
return
}
if _, err := os.Stat(path); err != nil {
facts[key] = DiskFact{}
return
}
hash, err := HashDiskPath(typ, path)
if err != nil {
hash = ""
}
facts[key] = DiskFact{Exists: true, Hash: hash}
}
for _, rec := range manifest.Items {
observe(rec.Type, rec.Name)
}
for _, it := range items {
observe(it.Type, it.Name)
}
for key := range oc.ScanTargetDirs() {
if _, seen := facts[key]; !seen {
facts[key] = DiskFact{Exists: true}
}
}
return facts
}
func printPlannedSyncReport(w io.Writer, r *SyncReport) {
fmt.Fprintf(w, "synced to revision %s\n", shortRev(r.Revision))
fmt.Fprintf(w, " deployed: %d\n", r.Deployed)
fmt.Fprintf(w, " updated: %d\n", r.Updated)
fmt.Fprintf(w, " kept: %d\n", r.Kept)
fmt.Fprintf(w, " removed: %d\n", r.Removed)
if r.Skipped > 0 {
fmt.Fprintf(w, " skipped (local modifications kept): %d\n", r.Skipped)
}
if r.Left > 0 {
fmt.Fprintf(w, " unmanaged items left untouched: %d\n", r.Left)
}
fmt.Fprintf(w, " manifest: %s\n", r.Manifest)
}
func shortRev(rev string) string {
if len(rev) > 12 {
return rev[:12]
}
return rev
}