Files
deployment-manager/internal/api/http.go
2026-02-01 20:22:29 +05:30

254 lines
5.8 KiB
Go

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