2026-08-23 10:31:00 +02:00
|
|
|
package client
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2026-08-23 10:36:44 +02:00
|
|
|
"strings"
|
2026-08-23 10:31:00 +02:00
|
|
|
|
|
|
|
|
"github.com/m3tam3re/agent-lib/internal/target"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// SyncOptions parameterize a client sync run.
|
|
|
|
|
type SyncOptions struct {
|
|
|
|
|
ConfigPath string
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 10:36:44 +02:00
|
|
|
// 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.
|
2026-08-23 10:31:00 +02:00
|
|
|
func Sync(opts SyncOptions, stdout io.Writer) (*SyncReport, 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 10:36:44 +02:00
|
|
|
items := make([]PlannedItem, 0, len(inv.Items))
|
2026-08-23 10:31:00 +02:00
|
|
|
for _, it := range inv.Deployable() {
|
2026-08-23 10:36:44 +02:00
|
|
|
if _, ok := oc.DeployPath(it.Type, it.Name); !ok {
|
2026-08-23 10:31:00 +02:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
hash, err := HashItem(pull.Tree, it)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("hashing %s/%s: %w", it.Type, it.Name, err)
|
|
|
|
|
}
|
2026-08-23 10:36:44 +02:00
|
|
|
items = append(items, PlannedItem{Type: it.Type, Name: it.Name, Origin: it.Origin, RelPath: it.RelPath, Hash: hash})
|
2026-08-23 10:31:00 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 10:36:44 +02:00
|
|
|
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)
|
2026-08-23 10:31:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 10:36:44 +02:00
|
|
|
return ExecutePlan(pull.Tree, plan, manifest, paths, pull.Rev, stdout)
|
2026-08-23 10:31:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// safePath rejects destinations that escape the user's home directory.
|
|
|
|
|
func safePath(dst string) bool {
|
|
|
|
|
home, err := os.UserHomeDir()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-08-23 10:36:44 +02:00
|
|
|
if dst == home {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-08-23 10:31:00 +02:00
|
|
|
rel, err := filepath.Rel(home, dst)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2026-08-23 10:36:44 +02:00
|
|
|
return rel != ".." && rel != "." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
2026-08-23 10:31:00 +02:00
|
|
|
}
|