iop/apps/edge/internal/bootstrap/refresh_admin.go
toki 6706b5e1da feat(edge): runtime reconnect config refresh 구현 및 테스트 개선
- Edge 노드 runtime 재연결 시 설정 리프레시 로직 구현
- configrefresh classify/result 모듈 개선
- bootstrap refresh_admin 및 runtime 관련 코드 refactor
- model_queue 및 edgecmd 테스트 개선
- 관련 test 파일의 assertion 및 mock 구조 개선
2026-06-21 23:19:05 +09:00

103 lines
2.6 KiB
Go

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)
}