package api import ( "context" "database/sql" "encoding/json" "fmt" "log" "net/http" "strconv" "strings" "deployment-manager/internal/db" "deployment-manager/internal/events" "deployment-manager/internal/model" ) type HTTPServer struct { repoStore *db.RepoStore eventBus *events.Bus server *http.Server } func NewHTTPServer(database *sql.DB, eventBus *events.Bus) *HTTPServer { return &HTTPServer{ repoStore: db.NewRepoStore(database), eventBus: eventBus, } } func (s *HTTPServer) Start(addr string) error { mux := http.NewServeMux() // API routes mux.HandleFunc("/api/repos", s.handleRepos) mux.HandleFunc("/api/repos/", s.handleRepo) mux.HandleFunc("/api/repos/", s.handleRepoActions) // SSE endpoint sseHandler := NewSSEHandler(s.eventBus) mux.Handle("/events", sseHandler) // Health check mux.HandleFunc("/health", s.handleHealth) s.server = &http.Server{ Addr: addr, Handler: mux, } log.Printf("HTTP server starting on %s", addr) return s.server.ListenAndServe() } func (s *HTTPServer) Shutdown(ctx context.Context) error { if s.server != nil { return s.server.Shutdown(ctx) } return nil } func (s *HTTPServer) handleRepos(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: s.getRepos(w, r) case http.MethodPost: s.createRepo(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (s *HTTPServer) handleRepo(w http.ResponseWriter, r *http.Request) { repoID, err := extractRepoID(r.URL.Path) if err != nil { http.Error(w, "Invalid repo ID", http.StatusBadRequest) return } switch r.Method { case http.MethodGet: s.getRepo(w, r, repoID) case http.MethodDelete: s.deleteRepo(w, r, repoID) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (s *HTTPServer) handleRepoActions(w http.ResponseWriter, r *http.Request) { repoID, err := extractRepoID(r.URL.Path) if err != nil { http.Error(w, "Invalid repo ID", http.StatusBadRequest) return } action := extractAction(r.URL.Path) switch r.Method { case http.MethodPost: switch action { case "stop": s.stopRepo(w, r, repoID) case "restart": s.restartRepo(w, r, repoID) default: http.Error(w, "Invalid action", http.StatusBadRequest) } default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func (s *HTTPServer) getRepos(w http.ResponseWriter, r *http.Request) { userID := r.URL.Query().Get("user_id") var repos []*model.Repo var err error if userID != "" { repos, err = s.repoStore.ListByUser(userID) } else { repos, err = s.repoStore.ListByStatus(model.StatusDeployed) } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(repos) } func (s *HTTPServer) getRepo(w http.ResponseWriter, r *http.Request, repoID int64) { repo, err := s.repoStore.Get(repoID) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(repo) } func (s *HTTPServer) createRepo(w http.ResponseWriter, r *http.Request) { var req struct { RepoURL string `json:"repo_url"` UserID string `json:"user_id"` Type model.RepoType `json:"type"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } repo := &model.Repo{ RepoURL: req.RepoURL, Status: model.StatusNeedToDeploy, UserID: req.UserID, Type: req.Type, } if err := s.repoStore.Create(repo); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Publish event s.eventBus.Publish(events.NewRepoEvent(repo, events.EventTypeRepoCreated)) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(repo) } func (s *HTTPServer) deleteRepo(w http.ResponseWriter, r *http.Request, repoID int64) { repo, err := s.repoStore.Get(repoID) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } // Update status to deleted for cleanup repo.Status = model.StatusDeleted if err := s.repoStore.Update(repo); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Publish event s.eventBus.Publish(events.NewRepoEvent(repo, events.EventTypeRepoDeleted)) w.WriteHeader(http.StatusNoContent) } func (s *HTTPServer) stopRepo(w http.ResponseWriter, r *http.Request, repoID int64) { repo, err := s.repoStore.Get(repoID) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } repo.Status = model.StatusStopped if err := s.repoStore.Update(repo); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Publish event s.eventBus.Publish(events.NewRepoEvent(repo, events.EventTypeRepoUpdated)) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(repo) } func (s *HTTPServer) restartRepo(w http.ResponseWriter, r *http.Request, repoID int64) { repo, err := s.repoStore.Get(repoID) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return } repo.Status = model.StatusRestarting if err := s.repoStore.Update(repo); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Publish event s.eventBus.Publish(events.NewRepoEvent(repo, events.EventTypeRepoUpdated)) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(repo) } func extractRepoID(path string) (int64, error) { parts := strings.Split(path, "/") if len(parts) < 4 { return 0, fmt.Errorf("invalid path") } return strconv.ParseInt(parts[3], 10, 64) } func extractAction(path string) string { parts := strings.Split(path, "/") if len(parts) >= 5 { return parts[4] } return "" }