7d6719da12
Replace direct npm.Client usage throughout the codebase with the proxy.Provider interface, enabling pluggable proxy backends. The deployer, API layer, and proxy manager now use provider-agnostic route management (ConfigureRoute/DeleteRoute) instead of NPM-specific API calls. Adds ProxyRouteID (string) to Instance model and ProxyProvider setting to Settings, with SQLite migrations for backward compatibility.
168 lines
5.0 KiB
Go
168 lines
5.0 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/alexei/docker-watcher/internal/events"
|
|
"github.com/alexei/docker-watcher/internal/stale"
|
|
"github.com/alexei/docker-watcher/internal/store"
|
|
)
|
|
|
|
// listStaleContainers handles GET /api/containers/stale.
|
|
func (s *Server) listStaleContainers(w http.ResponseWriter, r *http.Request) {
|
|
if s.staleScanner == nil {
|
|
respondError(w, http.StatusServiceUnavailable, "stale scanner not initialized")
|
|
return
|
|
}
|
|
|
|
staleInstances, err := s.staleScanner.FindStaleInstances(r.Context())
|
|
if err != nil {
|
|
slog.Error("failed to find stale containers", "error", err)
|
|
respondError(w, http.StatusInternalServerError, "failed to find stale containers")
|
|
return
|
|
}
|
|
|
|
if staleInstances == nil {
|
|
staleInstances = []stale.StaleInstance{}
|
|
}
|
|
respondJSON(w, http.StatusOK, staleInstances)
|
|
}
|
|
|
|
// cleanupStaleContainer handles POST /api/containers/stale/{id}/cleanup.
|
|
// Stops the Docker container, removes the NPM proxy, and deletes the instance from the store.
|
|
func (s *Server) cleanupStaleContainer(w http.ResponseWriter, r *http.Request) {
|
|
instanceID := chi.URLParam(r, "id")
|
|
|
|
inst, err := s.store.GetInstanceByID(instanceID)
|
|
if err != nil {
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
respondNotFound(w, "instance")
|
|
return
|
|
}
|
|
slog.Error("failed to get instance", "instance_id", instanceID, "error", err)
|
|
respondError(w, http.StatusInternalServerError, "failed to get instance")
|
|
return
|
|
}
|
|
|
|
// Don't remove instances already being cleaned up.
|
|
if inst.Status == "removing" {
|
|
respondError(w, http.StatusConflict, "instance is already being removed")
|
|
return
|
|
}
|
|
|
|
if err := s.cleanupInstance(r, inst); err != nil {
|
|
slog.Error("failed to cleanup instance", "instance_id", instanceID, "error", err)
|
|
respondError(w, http.StatusInternalServerError, "failed to cleanup instance")
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, map[string]string{"cleaned": instanceID})
|
|
}
|
|
|
|
// bulkCleanupStaleContainers handles POST /api/containers/stale/cleanup.
|
|
// Cleans up all currently stale containers.
|
|
func (s *Server) bulkCleanupStaleContainers(w http.ResponseWriter, r *http.Request) {
|
|
if s.staleScanner == nil {
|
|
respondError(w, http.StatusServiceUnavailable, "stale scanner not initialized")
|
|
return
|
|
}
|
|
|
|
staleInstances, err := s.staleScanner.FindStaleInstances(r.Context())
|
|
if err != nil {
|
|
slog.Error("failed to find stale containers for bulk cleanup", "error", err)
|
|
respondError(w, http.StatusInternalServerError, "failed to find stale containers")
|
|
return
|
|
}
|
|
|
|
var cleaned []string
|
|
var failed []string
|
|
|
|
for _, si := range staleInstances {
|
|
if si.Instance.Status == "removing" {
|
|
continue
|
|
}
|
|
if err := s.cleanupInstance(r, si.Instance); err != nil {
|
|
slog.Error("bulk stale cleanup failed",
|
|
"instance_id", si.Instance.ID, "error", err)
|
|
failed = append(failed, si.Instance.ID)
|
|
continue
|
|
}
|
|
cleaned = append(cleaned, si.Instance.ID)
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, map[string]any{
|
|
"cleaned": cleaned,
|
|
"failed": failed,
|
|
})
|
|
}
|
|
|
|
// cleanupInstance stops a Docker container, removes the NPM proxy, deletes
|
|
// the store record, and emits an event.
|
|
func (s *Server) cleanupInstance(r *http.Request, inst store.Instance) error {
|
|
ctx := r.Context()
|
|
|
|
// Mark as removing.
|
|
if err := s.store.UpdateInstanceStatus(inst.ID, "removing"); err != nil {
|
|
slog.Warn("stale cleanup: update status to removing", "instance_id", inst.ID, "error", err)
|
|
}
|
|
|
|
// Stop and remove Docker container.
|
|
if inst.ContainerID != "" {
|
|
if err := s.docker.StopContainer(ctx, inst.ContainerID, 10); err != nil {
|
|
slog.Warn("stale cleanup: stop container", "container_id", inst.ContainerID, "error", err)
|
|
}
|
|
if err := s.docker.RemoveContainer(ctx, inst.ContainerID, true); err != nil {
|
|
slog.Warn("stale cleanup: remove container", "container_id", inst.ContainerID, "error", err)
|
|
}
|
|
}
|
|
|
|
// Delete proxy route if present.
|
|
if inst.ProxyRouteID != "" {
|
|
if err := s.proxyProvider.DeleteRoute(ctx, inst.ProxyRouteID); err != nil {
|
|
slog.Warn("stale cleanup: delete proxy route", "route_id", inst.ProxyRouteID, "error", err)
|
|
}
|
|
}
|
|
|
|
// Delete instance record.
|
|
if err := s.store.DeleteInstance(inst.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Emit cleanup event.
|
|
s.emitStaleCleanupEvent(inst)
|
|
|
|
return nil
|
|
}
|
|
|
|
// emitStaleCleanupEvent publishes an event when a stale container is cleaned up.
|
|
func (s *Server) emitStaleCleanupEvent(inst store.Instance) {
|
|
msg := "Stale container cleaned up: " + inst.ID + " (tag: " + inst.ImageTag + ")"
|
|
|
|
evt, err := s.store.InsertEvent(store.EventLog{
|
|
Source: "stale_cleanup",
|
|
Severity: "info",
|
|
Message: msg,
|
|
Metadata: `{"instance_id":"` + inst.ID + `","project_id":"` + inst.ProjectID + `","stage_id":"` + inst.StageID + `"}`,
|
|
})
|
|
if err != nil {
|
|
slog.Error("stale cleanup: failed to persist event", "error", err)
|
|
return
|
|
}
|
|
|
|
s.eventBus.Publish(events.Event{
|
|
Type: events.EventLog,
|
|
Payload: events.EventLogPayload{
|
|
ID: evt.ID,
|
|
Source: "stale_cleanup",
|
|
Severity: "info",
|
|
Message: msg,
|
|
Metadata: evt.Metadata,
|
|
CreatedAt: evt.CreatedAt,
|
|
},
|
|
})
|
|
}
|