Files
tiny-forge/cmd/server/main.go
T
alexei.dolgolyov 757c72eea1 fix(docker-watcher): phase 8 security fixes
Remove webhook secret from logs and API response.
Add auth-pending note to router. Fix decrypt fallback that
would use ciphertext as auth token on decrypt failure.
2026-03-27 22:10:00 +03:00

143 lines
3.8 KiB
Go

package main
import (
"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() {
dataDir := envOrDefault("DATA_DIR", "./data")
if err := os.MkdirAll(dataDir, 0o755); err != nil {
log.Fatalf("create data directory: %v", err)
}
// Open database.
dbPath := filepath.Join(dataDir, "docker-watcher.db")
db, err := store.New(dbPath)
if err != nil {
log.Fatalf("open store: %v", err)
}
defer db.Close()
// 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)
}
// 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")
}
// 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 configured (use /api/settings/webhook-url to retrieve)")
// 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.
func envOrDefault(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}