This commit is contained in:
Kar
2026-02-01 20:22:29 +05:30
commit 52265ed4cc
30 changed files with 2058 additions and 0 deletions

12
internal/api/health.go Normal file
View File

@@ -0,0 +1,12 @@
package api
import (
"encoding/json"
"net/http"
)
func (s *HTTPServer) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

253
internal/api/http.go Normal file
View File

@@ -0,0 +1,253 @@
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 ""
}

78
internal/api/sse.go Normal file
View File

@@ -0,0 +1,78 @@
package api
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"deployment-manager/internal/events"
)
type SSEHandler struct {
eventBus *events.Bus
}
func NewSSEHandler(eventBus *events.Bus) *SSEHandler {
return &SSEHandler{eventBus: eventBus}
}
func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set SSE headers
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
// Create a context for this connection
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
// Subscribe to events
eventChan := h.eventBus.Subscribe(ctx)
// Send initial connection event
h.sendEvent(w, "connected", map[string]interface{}{
"message": "SSE connection established",
})
// Handle events
for {
select {
case <-ctx.Done():
log.Println("SSE client disconnected")
return
case event := <-eventChan:
if err := h.sendEvent(w, string(event.Type), event.Data); err != nil {
log.Printf("Failed to send SSE event: %v", err)
return
}
}
}
}
func (h *SSEHandler) sendEvent(w http.ResponseWriter, eventType string, data interface{}) error {
// Marshal data to JSON
jsonData, err := json.Marshal(data)
if err != nil {
return err
}
// Format as SSE event
event := fmt.Sprintf("event: %s\ndata: %s\n\n", eventType, jsonData)
// Write to response
_, err = w.Write([]byte(event))
if err != nil {
return err
}
// Flush to ensure immediate delivery
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
return nil
}