feat: lodge server, client and admin
This commit is contained in:
commit
95fc50e2c2
84 files changed
+7625
No files matched your search
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"lodge/internal/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
database := cmp.Or(os.Getenv("LODGE_DB"), "/app/storage/lodge.db")
|
||||
if err := os.MkdirAll(filepath.Dir(database), 0o700); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
store, err := server.OpenStore(database)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
config := server.Config{
|
||||
AdminToken: os.Getenv("ADMIN_TOKEN"),
|
||||
PublicURL: os.Getenv("PUBLIC_URL"),
|
||||
StaticDir: cmp.Or(os.Getenv("STATIC_DIR"), "/app/static"),
|
||||
ClientsDir: cmp.Or(os.Getenv("CLIENTS_DIR"), "/app/dl"),
|
||||
}
|
||||
if config.AdminToken == "" {
|
||||
log.Print("ADMIN_TOKEN is empty: no machine can be added until it is set")
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Addr: cmp.Or(os.Getenv("LISTEN"), ":8080"),
|
||||
Handler: server.New(store, config),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
log.Printf("listening on %s", httpServer.Addr)
|
||||
log.Fatal(httpServer.ListenAndServe())
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"lodge/internal/client"
|
||||
"lodge/internal/gitrepo"
|
||||
"lodge/internal/protocol"
|
||||
)
|
||||
|
||||
const usage = `lodge keeps agent files out of git and in sync across machines
|
||||
|
||||
lodge login <url> <code> connect this machine using a code from the admin page
|
||||
lodge install start the daemon, install the git hook and the agent skill
|
||||
lodge uninstall undo install
|
||||
lodge update replace this binary with the one the server ships
|
||||
lodge add [dir] start syncing a repository that was cloned before install
|
||||
lodge forget [dir] stop syncing a repository
|
||||
lodge sync [dir] sync once, for machines without a daemon
|
||||
lodge status [dir] rules, files and oversized files of a repository
|
||||
lodge log [path] revisions of the repository or of one file
|
||||
lodge show <rev|sha> <path> a file as of a revision or a commit of the main repository
|
||||
lodge daemon run the sync daemon in the foreground
|
||||
|
||||
status, log and sync accept --json
|
||||
`
|
||||
|
||||
var hookEnvironment = []string{"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_PREFIX"}
|
||||
|
||||
type status struct {
|
||||
Root string `json:"root"`
|
||||
Key string `json:"key"`
|
||||
Branch string `json:"branch"`
|
||||
Rules []client.Rule `json:"rules"`
|
||||
Revision int64 `json:"revision"`
|
||||
Spaces map[string]protocol.Manifest `json:"spaces"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Print(usage)
|
||||
return
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
if os.Args[1] == "hook" {
|
||||
hook(ctx)
|
||||
return
|
||||
}
|
||||
err := run(ctx, os.Args[1], os.Args[2:])
|
||||
stop()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "lodge:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func hook(ctx context.Context) {
|
||||
for _, name := range hookEnvironment {
|
||||
os.Unsetenv(name)
|
||||
}
|
||||
if c, err := client.Load(); err == nil {
|
||||
_ = c.Hook(ctx, ".")
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context, name string, args []string) error {
|
||||
flags := flag.NewFlagSet(name, flag.ExitOnError)
|
||||
asJSON := flags.Bool("json", false, "machine-readable output")
|
||||
_ = flags.Parse(args)
|
||||
args = flags.Args()
|
||||
target := func(fallback string) string {
|
||||
if len(args) > 0 {
|
||||
return args[0]
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "login":
|
||||
if len(args) != 2 {
|
||||
return errors.New("usage: lodge login <url> <code>")
|
||||
}
|
||||
return client.Login(ctx, args[0], args[1])
|
||||
case "help", "-h", "--help":
|
||||
fmt.Print(usage)
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := client.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch name {
|
||||
case "install":
|
||||
return c.Install()
|
||||
case "uninstall":
|
||||
return c.Uninstall()
|
||||
case "daemon":
|
||||
return c.Daemon(ctx)
|
||||
case "update":
|
||||
updated, err := c.Update(ctx)
|
||||
if err == nil && !updated {
|
||||
fmt.Println("already up to date")
|
||||
}
|
||||
return err
|
||||
case "add", "sync":
|
||||
res, err := c.Sync(ctx, target("."), true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return output(*asJSON, res, func() {
|
||||
fmt.Printf("pushed %d, pulled %d\n", res.Pushed, res.Pulled)
|
||||
for _, path := range res.Skipped {
|
||||
fmt.Printf("skipped, over 5 MB: %s\n", path)
|
||||
}
|
||||
})
|
||||
case "forget":
|
||||
root, err := filepath.Abs(target("."))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if repo, err := gitrepo.Open(root); err == nil {
|
||||
root = repo.Root
|
||||
}
|
||||
return c.Forget(root)
|
||||
case "status":
|
||||
return showStatus(ctx, c, target("."), *asJSON)
|
||||
case "log":
|
||||
return showLog(ctx, c, target(""), *asJSON)
|
||||
case "show":
|
||||
if len(args) != 2 {
|
||||
return errors.New("usage: lodge show <revision|commit-sha> <path>")
|
||||
}
|
||||
return showFile(ctx, c, args[0], args[1])
|
||||
}
|
||||
return fmt.Errorf("unknown command %q, see lodge help", name)
|
||||
}
|
||||
|
||||
func output(asJSON bool, value any, human func()) error {
|
||||
if !asJSON {
|
||||
human()
|
||||
return nil
|
||||
}
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(value)
|
||||
}
|
||||
|
||||
func showStatus(ctx context.Context, c *client.Client, dir string, asJSON bool) error {
|
||||
repo, err := gitrepo.Open(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state, err := c.State(ctx, repo.Key, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report := status{Root: repo.Root, Key: repo.Key, Branch: repo.Branch(), Rules: client.LoadRules(repo.Root), Revision: state.Seq, Spaces: state.Spaces}
|
||||
return output(asJSON, report, func() {
|
||||
fmt.Printf("%s\nkey %s, branch %s, revision %d\n\nrules:\n", report.Root, report.Key, report.Branch, report.Revision)
|
||||
for _, rule := range report.Rules {
|
||||
kind := "branch"
|
||||
if rule.Shared {
|
||||
kind = "shared"
|
||||
}
|
||||
fmt.Printf(" %s %s\n", kind, rule.Path)
|
||||
}
|
||||
for space, manifest := range report.Spaces {
|
||||
fmt.Printf("\n%s:\n", space)
|
||||
for path := range manifest {
|
||||
fmt.Printf(" %s\n", path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func showLog(ctx context.Context, c *client.Client, path string, asJSON bool) error {
|
||||
repo, err := gitrepo.Open(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
revisions, err := c.Revisions(ctx, repo.Key, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return output(asJSON, revisions, func() {
|
||||
for _, rev := range revisions {
|
||||
when := time.UnixMilli(rev.Time).Format("2006-01-02 15:04")
|
||||
fmt.Printf("%d %s %s %s@%.10s\n", rev.Seq, when, rev.Machine, rev.MainBranch, rev.MainSHA)
|
||||
for _, change := range rev.Changes {
|
||||
action := "M"
|
||||
if change.Base == "" {
|
||||
action = "A"
|
||||
} else if change.Hash == "" {
|
||||
action = "D"
|
||||
}
|
||||
fmt.Printf(" %s %s (%s)\n", action, change.Path, change.Space)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func showFile(ctx context.Context, c *client.Client, at, path string) error {
|
||||
repo, err := gitrepo.Open(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state, err := c.State(ctx, repo.Key, at)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, space := range []string{protocol.SharedSpace, protocol.BranchSpace(repo.Branch())} {
|
||||
if file, found := state.Spaces[space][filepath.ToSlash(path)]; found {
|
||||
data, err := c.Blob(ctx, file.Hash)
|
||||
if err == nil {
|
||||
_, err = os.Stdout.Write(data)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%s did not exist at %s", path, at)
|
||||
}
|
||||
Reference in new issue
Block a user