package bootstrap import ( "context" "encoding/json" "net" "net/http" "time" "go.uber.org/zap" "iop/apps/edge/internal/configrefresh" ) // RefreshAdminServer is the Edge-local HTTP admin server for config refresh. // It listens on a loopback address and accepts POST /refresh requests from // the local CLI (iop-edge config refresh). The server is only started when // refresh.enabled=true in the Edge config. type RefreshAdminServer struct { listen string runtime *Runtime logger *zap.Logger server *http.Server ln net.Listener } func newRefreshAdminServer(listen string, rt *Runtime, logger *zap.Logger) *RefreshAdminServer { return &RefreshAdminServer{ listen: listen, runtime: rt, logger: logger, } } func (s *RefreshAdminServer) Start(_ context.Context) error { ln, err := net.Listen("tcp", s.listen) if err != nil { return err } s.ln = ln mux := http.NewServeMux() mux.HandleFunc("/refresh", s.handleRefresh) s.server = &http.Server{ Handler: mux, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, } go func() { if err := s.server.Serve(ln); err != nil && err != http.ErrServerClosed { s.logger.Warn("refresh admin server exited", zap.Error(err)) } }() s.logger.Info("refresh admin server listening", zap.String("addr", ln.Addr().String())) return nil } func (s *RefreshAdminServer) Stop(ctx context.Context) error { if s.server == nil { return nil } return s.server.Shutdown(ctx) } // Addr returns the actual bound address. Useful for port-0 tests. func (s *RefreshAdminServer) Addr() string { if s.ln == nil { return "" } return s.ln.Addr().String() } func (s *RefreshAdminServer) handleRefresh(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req configrefresh.Request if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, configrefresh.RejectedResult(req, "invalid request body: "+err.Error())) return } result, err := s.runtime.RefreshConfig(r.Context(), req) if err != nil { writeJSON(w, http.StatusInternalServerError, configrefresh.RejectedResult(req, "refresh error: "+err.Error())) return } httpStatus := http.StatusOK if result.Status == configrefresh.StatusRejected { httpStatus = http.StatusUnprocessableEntity } writeJSON(w, httpStatus, result) } func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) }