d4659146fc
Per-stage env var overrides with encryption for secrets.
Volume mounts with shared/isolated modes (isolated appends
/{stage}-{tag}/ to source path). Store CRUD, API endpoints,
and frontend editors for both. Env merge during deploy.
175 lines
5.0 KiB
Go
175 lines
5.0 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/alexei/docker-watcher/internal/auth"
|
|
"github.com/alexei/docker-watcher/internal/docker"
|
|
"github.com/alexei/docker-watcher/internal/events"
|
|
"github.com/alexei/docker-watcher/internal/store"
|
|
"github.com/alexei/docker-watcher/internal/webhook"
|
|
)
|
|
|
|
// Server holds all dependencies for the API layer.
|
|
type Server struct {
|
|
store *store.Store
|
|
docker *docker.Client
|
|
deployer DeployTriggerer
|
|
webhook *webhook.Handler
|
|
eventBus *events.Bus
|
|
encKey [32]byte
|
|
localAuth *auth.LocalAuth
|
|
oidcProvider *auth.OIDCProvider
|
|
}
|
|
|
|
// NewServer creates a new API Server with all required dependencies.
|
|
func NewServer(
|
|
st *store.Store,
|
|
dockerClient *docker.Client,
|
|
deployer DeployTriggerer,
|
|
webhookHandler *webhook.Handler,
|
|
eventBus *events.Bus,
|
|
encKey [32]byte,
|
|
) *Server {
|
|
localAuth := auth.NewLocalAuth(encKey)
|
|
|
|
s := &Server{
|
|
store: st,
|
|
docker: dockerClient,
|
|
deployer: deployer,
|
|
webhook: webhookHandler,
|
|
eventBus: eventBus,
|
|
encKey: encKey,
|
|
localAuth: localAuth,
|
|
}
|
|
|
|
// Try to initialize OIDC provider from stored settings.
|
|
authSettings, err := st.GetAuthSettings()
|
|
if err == nil && authSettings.AuthMode == "oidc" && authSettings.OIDCIssuerURL != "" {
|
|
s.initOIDCProvider(context.Background(), authSettings)
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// initOIDCProvider creates an OIDC provider from settings. Errors are logged, not fatal.
|
|
func (s *Server) initOIDCProvider(ctx context.Context, as store.AuthSettings) {
|
|
provider, err := auth.NewOIDCProvider(ctx, auth.OIDCConfig{
|
|
IssuerURL: as.OIDCIssuerURL,
|
|
ClientID: as.OIDCClientID,
|
|
ClientSecret: as.OIDCClientSecret,
|
|
RedirectURL: as.OIDCRedirectURL,
|
|
})
|
|
if err != nil {
|
|
slog.Warn("failed to initialize OIDC provider", "error", err)
|
|
return
|
|
}
|
|
s.oidcProvider = provider
|
|
slog.Info("OIDC provider initialized", "issuer", as.OIDCIssuerURL)
|
|
}
|
|
|
|
// Router returns a chi router with all API routes mounted.
|
|
func (s *Server) Router() chi.Router {
|
|
r := chi.NewRouter()
|
|
|
|
// Global middleware.
|
|
r.Use(recovery)
|
|
r.Use(logging)
|
|
r.Use(cors)
|
|
r.Use(jsonContentType)
|
|
|
|
r.Route("/api", func(r chi.Router) {
|
|
// Public auth endpoints (no auth required).
|
|
r.Post("/auth/login", s.login)
|
|
r.Get("/auth/oidc/login", s.oidcLogin)
|
|
r.Get("/auth/oidc/callback", s.oidcCallback)
|
|
|
|
// Webhook handler (uses its own secret-based auth).
|
|
r.Mount("/webhook", s.webhook.Route())
|
|
|
|
// Config export (public endpoint, useful for backup).
|
|
r.Get("/config/export", s.exportConfig)
|
|
|
|
// Protected routes: require valid JWT.
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(auth.Middleware(s.localAuth))
|
|
|
|
// Auth management.
|
|
r.Get("/auth/me", s.currentUser)
|
|
r.Get("/auth/settings", s.getAuthSettings)
|
|
r.Put("/auth/settings", s.updateAuthSettings)
|
|
r.Get("/auth/users", s.listUsers)
|
|
r.Post("/auth/users", s.createUser)
|
|
r.Delete("/auth/users/{uid}", s.deleteUser)
|
|
|
|
// Project endpoints.
|
|
r.Get("/projects", s.listProjects)
|
|
r.Post("/projects", s.createProject)
|
|
r.Route("/projects/{id}", func(r chi.Router) {
|
|
r.Get("/", s.getProject)
|
|
r.Put("/", s.updateProject)
|
|
r.Delete("/", s.deleteProject)
|
|
|
|
// Stage endpoints.
|
|
r.Post("/stages", s.createStage)
|
|
r.Put("/stages/{stage}", s.updateStage)
|
|
r.Delete("/stages/{stage}", s.deleteStage)
|
|
|
|
// Stage env override endpoints.
|
|
r.Get("/stages/{stage}/env", s.listStageEnv)
|
|
r.Post("/stages/{stage}/env", s.createStageEnv)
|
|
r.Put("/stages/{stage}/env/{envId}", s.updateStageEnv)
|
|
r.Delete("/stages/{stage}/env/{envId}", s.deleteStageEnv)
|
|
|
|
// Instance endpoints.
|
|
r.Get("/stages/{stage}/instances", s.listInstances)
|
|
r.Post("/stages/{stage}/instances", s.deployInstance)
|
|
r.Delete("/stages/{stage}/instances/{iid}", s.removeInstance)
|
|
|
|
// Instance control endpoints.
|
|
r.Post("/stages/{stage}/instances/{iid}/stop", s.stopInstance)
|
|
r.Post("/stages/{stage}/instances/{iid}/start", s.startInstance)
|
|
r.Post("/stages/{stage}/instances/{iid}/restart", s.restartInstance)
|
|
|
|
// Volume endpoints.
|
|
r.Get("/volumes", s.listVolumes)
|
|
r.Post("/volumes", s.createVolume)
|
|
r.Put("/volumes/{volId}", s.updateVolume)
|
|
r.Delete("/volumes/{volId}", s.deleteVolume)
|
|
})
|
|
|
|
// Deploy endpoints.
|
|
r.Get("/deploys", s.listDeploys)
|
|
r.Get("/deploys/{id}/logs", s.streamDeployLogs)
|
|
|
|
// SSE endpoint for real-time instance status and deploy events.
|
|
r.Get("/events", s.streamEvents)
|
|
|
|
// Quick deploy endpoints.
|
|
r.Post("/deploy/inspect", s.inspectImage)
|
|
r.Post("/deploy/quick", s.quickDeploy)
|
|
|
|
// Registry endpoints.
|
|
r.Get("/registries", s.listRegistries)
|
|
r.Post("/registries", s.createRegistry)
|
|
r.Route("/registries/{id}", func(r chi.Router) {
|
|
r.Put("/", s.updateRegistry)
|
|
r.Delete("/", s.deleteRegistry)
|
|
r.Post("/test", s.testRegistry)
|
|
r.Get("/tags/*", s.listRegistryTags)
|
|
})
|
|
|
|
// Settings endpoints.
|
|
r.Get("/settings", s.getSettings)
|
|
r.Put("/settings", s.updateSettings)
|
|
r.Get("/settings/webhook-url", s.getWebhookURL)
|
|
r.Post("/settings/regenerate", s.regenerateWebhookSecret)
|
|
})
|
|
})
|
|
|
|
return r
|
|
}
|