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:
2026-03-27 22:06:57 +03:00
parent bbcc4f55f0
commit 97d4243cfe
9 changed files with 1211 additions and 25 deletions
+102 -4
View File
@@ -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.