ScuttleBot
feat: wire daemon — ergo lifecycle, registry, REST API server Boots ergo, waits for health, wires registry + HTTP API on :8080. Generates and prints API token on startup, graceful SIGINT/SIGTERM shutdown. Closes #21
Commit
b781baa40cb6bb76cd62b5eeb8af83eaaa4f74a9e85abf17d41f3971706247bd
Parent
7ddb0c47739857b…
1 file changed
+97
-2
+97
-2
| --- cmd/scuttlebot/main.go | ||
| +++ cmd/scuttlebot/main.go | ||
| @@ -1,9 +1,104 @@ | ||
| 1 | 1 | package main |
| 2 | 2 | |
| 3 | -import "fmt" | |
| 3 | +import ( | |
| 4 | + "context" | |
| 5 | + "crypto/rand" | |
| 6 | + "encoding/hex" | |
| 7 | + "fmt" | |
| 8 | + "log/slog" | |
| 9 | + "net/http" | |
| 10 | + "os" | |
| 11 | + "os/signal" | |
| 12 | + "syscall" | |
| 13 | + "time" | |
| 14 | + | |
| 15 | + "github.com/conflicthq/scuttlebot/internal/api" | |
| 16 | + "github.com/conflicthq/scuttlebot/internal/config" | |
| 17 | + "github.com/conflicthq/scuttlebot/internal/ergo" | |
| 18 | + "github.com/conflicthq/scuttlebot/internal/registry" | |
| 19 | +) | |
| 4 | 20 | |
| 5 | 21 | var version = "dev" |
| 6 | 22 | |
| 7 | 23 | func main() { |
| 8 | - fmt.Printf("scuttlebot %s\n", version) | |
| 24 | + log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) | |
| 25 | + | |
| 26 | + cfg := &config.Config{} | |
| 27 | + cfg.Defaults() | |
| 28 | + | |
| 29 | + // Generate an API token for the Ergo management API if not set. | |
| 30 | + if cfg.Ergo.APIToken == "" { | |
| 31 | + cfg.Ergo.APIToken = mustGenToken() | |
| 32 | + } | |
| 33 | + | |
| 34 | + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | |
| 35 | + defer cancel() | |
| 36 | + | |
| 37 | + log.Info("scuttlebot starting", "version", version) | |
| 38 | + | |
| 39 | + // Start Ergo. | |
| 40 | + manager := ergo.NewManager(cfg.Ergo, log) | |
| 41 | + ergoErr := make(chan error, 1) | |
| 42 | + go func() { | |
| 43 | + if err := manager.Start(ctx); err != nil { | |
| 44 | + ergoErr <- err | |
| 45 | + } | |
| 46 | + }() | |
| 47 | + | |
| 48 | + // Wait for Ergo to become healthy before starting the rest. | |
| 49 | + healthCtx, healthCancel := context.WithTimeout(ctx, 30*time.Second) | |
| 50 | + defer healthCancel() | |
| 51 | + for { | |
| 52 | + if _, err := manager.API().Status(); err == nil { | |
| 53 | + break | |
| 54 | + } | |
| 55 | + select { | |
| 56 | + case <-healthCtx.Done(): | |
| 57 | + log.Error("ergo did not become healthy in time") | |
| 58 | + os.Exit(1) | |
| 59 | + case err := <-ergoErr: | |
| 60 | + log.Error("ergo failed to start", "err", err) | |
| 61 | + os.Exit(1) | |
| 62 | + case <-time.After(500 * time.Millisecond): | |
| 63 | + } | |
| 64 | + } | |
| 65 | + log.Info("ergo healthy") | |
| 66 | + | |
| 67 | + // Build registry backed by Ergo's NickServ API. | |
| 68 | + signingKey := []byte(mustGenToken()) | |
| 69 | + reg := registry.New(manager.API(), signingKey) | |
| 70 | + | |
| 71 | + // Start HTTP API server. | |
| 72 | + apiToken := mustGenToken() | |
| 73 | + log.Info("api token", "token", apiToken) // printed once on startup — user copies this | |
| 74 | + apiSrv := api.New(reg, []string{apiToken}, log) | |
| 75 | + httpServer := &http.Server{ | |
| 76 | + Addr: ":8080", | |
| 77 | + Handler: apiSrv.Handler(), | |
| 78 | + } | |
| 79 | + | |
| 80 | + go func() { | |
| 81 | + log.Info("api server listening", "addr", httpServer.Addr) | |
| 82 | + if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { | |
| 83 | + log.Error("api server error", "err", err) | |
| 84 | + } | |
| 85 | + }() | |
| 86 | + | |
| 87 | + <-ctx.Done() | |
| 88 | + log.Info("shutting down") | |
| 89 | + | |
| 90 | + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) | |
| 91 | + defer shutdownCancel() | |
| 92 | + _ = httpServer.Shutdown(shutdownCtx) | |
| 93 | + | |
| 94 | + log.Info("goodbye") | |
| 95 | +} | |
| 96 | + | |
| 97 | +func mustGenToken() string { | |
| 98 | + b := make([]byte, 24) | |
| 99 | + if _, err := rand.Read(b); err != nil { | |
| 100 | + fmt.Fprintf(os.Stderr, "failed to generate token: %v\n", err) | |
| 101 | + os.Exit(1) | |
| 102 | + } | |
| 103 | + return hex.EncodeToString(b) | |
| 9 | 104 | } |
| 10 | 105 |
| --- cmd/scuttlebot/main.go | |
| +++ cmd/scuttlebot/main.go | |
| @@ -1,9 +1,104 @@ | |
| 1 | package main |
| 2 | |
| 3 | import "fmt" |
| 4 | |
| 5 | var version = "dev" |
| 6 | |
| 7 | func main() { |
| 8 | fmt.Printf("scuttlebot %s\n", version) |
| 9 | } |
| 10 |
| --- cmd/scuttlebot/main.go | |
| +++ cmd/scuttlebot/main.go | |
| @@ -1,9 +1,104 @@ | |
| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/rand" |
| 6 | "encoding/hex" |
| 7 | "fmt" |
| 8 | "log/slog" |
| 9 | "net/http" |
| 10 | "os" |
| 11 | "os/signal" |
| 12 | "syscall" |
| 13 | "time" |
| 14 | |
| 15 | "github.com/conflicthq/scuttlebot/internal/api" |
| 16 | "github.com/conflicthq/scuttlebot/internal/config" |
| 17 | "github.com/conflicthq/scuttlebot/internal/ergo" |
| 18 | "github.com/conflicthq/scuttlebot/internal/registry" |
| 19 | ) |
| 20 | |
| 21 | var version = "dev" |
| 22 | |
| 23 | func main() { |
| 24 | log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) |
| 25 | |
| 26 | cfg := &config.Config{} |
| 27 | cfg.Defaults() |
| 28 | |
| 29 | // Generate an API token for the Ergo management API if not set. |
| 30 | if cfg.Ergo.APIToken == "" { |
| 31 | cfg.Ergo.APIToken = mustGenToken() |
| 32 | } |
| 33 | |
| 34 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) |
| 35 | defer cancel() |
| 36 | |
| 37 | log.Info("scuttlebot starting", "version", version) |
| 38 | |
| 39 | // Start Ergo. |
| 40 | manager := ergo.NewManager(cfg.Ergo, log) |
| 41 | ergoErr := make(chan error, 1) |
| 42 | go func() { |
| 43 | if err := manager.Start(ctx); err != nil { |
| 44 | ergoErr <- err |
| 45 | } |
| 46 | }() |
| 47 | |
| 48 | // Wait for Ergo to become healthy before starting the rest. |
| 49 | healthCtx, healthCancel := context.WithTimeout(ctx, 30*time.Second) |
| 50 | defer healthCancel() |
| 51 | for { |
| 52 | if _, err := manager.API().Status(); err == nil { |
| 53 | break |
| 54 | } |
| 55 | select { |
| 56 | case <-healthCtx.Done(): |
| 57 | log.Error("ergo did not become healthy in time") |
| 58 | os.Exit(1) |
| 59 | case err := <-ergoErr: |
| 60 | log.Error("ergo failed to start", "err", err) |
| 61 | os.Exit(1) |
| 62 | case <-time.After(500 * time.Millisecond): |
| 63 | } |
| 64 | } |
| 65 | log.Info("ergo healthy") |
| 66 | |
| 67 | // Build registry backed by Ergo's NickServ API. |
| 68 | signingKey := []byte(mustGenToken()) |
| 69 | reg := registry.New(manager.API(), signingKey) |
| 70 | |
| 71 | // Start HTTP API server. |
| 72 | apiToken := mustGenToken() |
| 73 | log.Info("api token", "token", apiToken) // printed once on startup — user copies this |
| 74 | apiSrv := api.New(reg, []string{apiToken}, log) |
| 75 | httpServer := &http.Server{ |
| 76 | Addr: ":8080", |
| 77 | Handler: apiSrv.Handler(), |
| 78 | } |
| 79 | |
| 80 | go func() { |
| 81 | log.Info("api server listening", "addr", httpServer.Addr) |
| 82 | if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { |
| 83 | log.Error("api server error", "err", err) |
| 84 | } |
| 85 | }() |
| 86 | |
| 87 | <-ctx.Done() |
| 88 | log.Info("shutting down") |
| 89 | |
| 90 | shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) |
| 91 | defer shutdownCancel() |
| 92 | _ = httpServer.Shutdown(shutdownCtx) |
| 93 | |
| 94 | log.Info("goodbye") |
| 95 | } |
| 96 | |
| 97 | func mustGenToken() string { |
| 98 | b := make([]byte, 24) |
| 99 | if _, err := rand.Read(b); err != nil { |
| 100 | fmt.Fprintf(os.Stderr, "failed to generate token: %v\n", err) |
| 101 | os.Exit(1) |
| 102 | } |
| 103 | return hex.EncodeToString(b) |
| 104 | } |
| 105 |