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

112 lines
2.9 KiB
Go
Raw Normal View History

package client
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/m3tam3re/agent-lib/internal/target"
)
// SyncOptions parameterize a client sync run.
type SyncOptions struct {
ConfigPath string
}
// prepared is everything sync and status share: the pulled repository, the
// planned action list, and the local state — before any mutation.
type prepared struct {
paths StatePaths
pull *PullResult
manifest *Manifest
items []PlannedItem
plan []PlanEntry
}
// prepare pulls, inventories, hashes and plans — the complete read-only
// front half of a client run.
func prepare(opts SyncOptions) (*prepared, error) {
cfg, err := LoadClientConfig(opts.ConfigPath)
if err != nil {
return nil, err
}
paths, err := ResolveStatePaths()
if err != nil {
return nil, err
}
if err := os.MkdirAll(paths.Root, 0o755); err != nil {
return nil, err
}
pull, err := Pull(paths.CacheDir, cfg.RepoURL, cfg.Ref, cfg.Token, cfg.TokenUser)
if err != nil {
return nil, err
}
inv, err := BuildInventory(pull.Tree, pull.Rev)
if err != nil {
return nil, err
}
oc, err := target.NewOpenCode()
if err != nil {
return nil, err
}
manifest, err := LoadManifest(paths.Manifest)
if err != nil {
return nil, err
}
items := make([]PlannedItem, 0, len(inv.Items))
for _, it := range inv.Deployable() {
if _, ok := oc.DeployPath(it.Type, it.Name); !ok {
continue
}
hash, err := HashItem(pull.Tree, it)
if err != nil {
return nil, fmt.Errorf("hashing %s/%s: %w", it.Type, it.Name, err)
}
items = append(items, PlannedItem{Type: it.Type, Name: it.Name, Origin: it.Origin, RelPath: it.RelPath, Hash: hash})
}
pathFor := func(typ, name string) string {
path, _ := oc.DeployPath(typ, name)
return path
}
plan := Plan(items, manifest, diskFacts(oc, manifest, items), pathFor)
for _, e := range plan {
if !safePath(e.DstPath) {
return nil, fmt.Errorf("refusing unsafe deploy path %q", e.DstPath)
}
}
return &prepared{paths: paths, pull: pull, manifest: manifest, items: items, plan: plan}, nil
}
// Sync pulls the work repository, plans the sync as a pure function over
// (inventory, manifest, disk facts) and executes the plan. Validation and
// hashing happen before the first mutation; unreachable repositories fail
// cleanly with the previous state intact.
func Sync(opts SyncOptions, stdout io.Writer) (*SyncReport, error) {
p, err := prepare(opts)
if err != nil {
return nil, err
}
return ExecutePlan(p.pull.Tree, p.plan, p.manifest, p.paths, p.pull.Rev, stdout)
}
// safePath rejects destinations that escape the user's home directory.
func safePath(dst string) bool {
home, err := os.UserHomeDir()
if err != nil {
return false
}
if dst == home {
return false
}
rel, err := filepath.Rel(home, dst)
if err != nil {
return false
}
return rel != ".." && rel != "." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
}