feat(docker-watcher): phase 8 - REST API layer
All REST endpoints wired with chi router: projects, stages, instances, deploys, registries, settings, quick deploy, webhook. Full main.go wiring with graceful shutdown. Consistent JSON envelope responses. Sensitive fields stripped from API responses.
This commit is contained in:
+102
-4
@@ -1,13 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/alexei/docker-watcher/internal/api"
|
||||
"github.com/alexei/docker-watcher/internal/config"
|
||||
"github.com/alexei/docker-watcher/internal/crypto"
|
||||
"github.com/alexei/docker-watcher/internal/deployer"
|
||||
"github.com/alexei/docker-watcher/internal/docker"
|
||||
"github.com/alexei/docker-watcher/internal/health"
|
||||
"github.com/alexei/docker-watcher/internal/notify"
|
||||
"github.com/alexei/docker-watcher/internal/npm"
|
||||
"github.com/alexei/docker-watcher/internal/registry"
|
||||
"github.com/alexei/docker-watcher/internal/store"
|
||||
"github.com/alexei/docker-watcher/internal/webhook"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -17,6 +30,7 @@ func main() {
|
||||
log.Fatalf("create data directory: %v", err)
|
||||
}
|
||||
|
||||
// Open database.
|
||||
dbPath := filepath.Join(dataDir, "docker-watcher.db")
|
||||
db, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
@@ -24,15 +38,99 @@ func main() {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Import seed config on first launch (idempotent — skipped if DB has data).
|
||||
// Import seed config on first launch (idempotent).
|
||||
seedPath := envOrDefault("SEED_FILE", "./docker-watcher.yaml")
|
||||
if err := config.ImportSeed(db, seedPath); err != nil {
|
||||
log.Fatalf("seed import: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Docker Watcher started. Database: %s\n", dbPath)
|
||||
// Derive encryption key from environment.
|
||||
encKey, err := crypto.KeyFromEnv()
|
||||
if err != nil {
|
||||
log.Printf("WARNING: %v — encrypted fields will not work", err)
|
||||
encKey = crypto.DeriveKey("docker-watcher-default-key")
|
||||
}
|
||||
|
||||
// Future phases will wire up the HTTP server, deployer, poller, etc.
|
||||
// Initialize Docker client.
|
||||
dockerClient, err := docker.New()
|
||||
if err != nil {
|
||||
log.Fatalf("create docker client: %v", err)
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
// Read settings for NPM URL and polling interval.
|
||||
settings, err := db.GetSettings()
|
||||
if err != nil {
|
||||
log.Fatalf("get settings: %v", err)
|
||||
}
|
||||
|
||||
// Initialize NPM client.
|
||||
npmURL := envOrDefault("NPM_URL", settings.NpmURL)
|
||||
npmClient := npm.New(npmURL)
|
||||
|
||||
// Initialize services.
|
||||
healthChecker := health.New()
|
||||
notifier := notify.New()
|
||||
|
||||
dep := deployer.New(dockerClient, npmClient, db, healthChecker, notifier, encKey)
|
||||
|
||||
// Initialize webhook handler.
|
||||
webhookHandler := webhook.NewHandler(db, dep, dockerClient)
|
||||
|
||||
// Ensure webhook secret exists.
|
||||
secret, err := webhook.EnsureWebhookSecret(db)
|
||||
if err != nil {
|
||||
log.Fatalf("ensure webhook secret: %v", err)
|
||||
}
|
||||
log.Printf("Webhook secret: %s", secret)
|
||||
|
||||
// Initialize registry poller.
|
||||
poller := registry.NewPoller(db, dep, encKey)
|
||||
pollingInterval := envOrDefault("POLLING_INTERVAL", settings.PollingInterval)
|
||||
if pollingInterval != "" {
|
||||
if err := poller.Start(pollingInterval); err != nil {
|
||||
log.Printf("WARNING: failed to start poller: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Build API server.
|
||||
apiServer := api.NewServer(db, dockerClient, dep, webhookHandler, encKey)
|
||||
router := apiServer.Router()
|
||||
|
||||
// Start HTTP server.
|
||||
addr := envOrDefault("LISTEN_ADDR", ":8080")
|
||||
httpServer := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: router,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
// Graceful shutdown.
|
||||
done := make(chan os.Signal, 1)
|
||||
signal.Notify(done, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
log.Printf("Docker Watcher started. Listening on %s", addr)
|
||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("HTTP server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-done
|
||||
log.Println("Shutting down...")
|
||||
|
||||
poller.Stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := httpServer.Shutdown(ctx); err != nil {
|
||||
log.Printf("HTTP server shutdown error: %v", err)
|
||||
}
|
||||
|
||||
log.Println("Docker Watcher stopped.")
|
||||
}
|
||||
|
||||
// envOrDefault reads an environment variable or returns the fallback value.
|
||||
|
||||
Reference in New Issue
Block a user