chore: sync from fn-registry agent
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func cmdServe(args []string) {
|
||||
fs := flag.NewFlagSet("serve", flag.ExitOnError)
|
||||
port := fs.Int("port", 9090, "listen port")
|
||||
fs.Parse(args)
|
||||
|
||||
s := openStoreOrDie()
|
||||
// No defer close — server runs forever
|
||||
|
||||
d := NewDeployer(s, registryRoot())
|
||||
srv := &Server{store: s, deployer: d}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /webhook/push", srv.handleWebhook)
|
||||
mux.HandleFunc("GET /api/targets", srv.handleListTargets)
|
||||
mux.HandleFunc("GET /api/targets/{app}", srv.handleGetTarget)
|
||||
mux.HandleFunc("POST /api/deploy/{app}", srv.handleDeploy)
|
||||
mux.HandleFunc("GET /api/status/{app}", srv.handleStatus)
|
||||
mux.HandleFunc("GET /api/health", srv.handleHealth)
|
||||
mux.HandleFunc("GET /api/logs", srv.handleLogs)
|
||||
mux.HandleFunc("GET /api/logs/{app}", srv.handleLogs)
|
||||
|
||||
addr := fmt.Sprintf(":%d", *port)
|
||||
fmt.Printf("deploy_server listening on %s\n", addr)
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "server error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
store *Store
|
||||
deployer *Deployer
|
||||
}
|
||||
|
||||
// giteaPushPayload es el subset del payload de Gitea que nos interesa.
|
||||
type giteaPushPayload struct {
|
||||
Ref string `json:"ref"`
|
||||
Repository struct {
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
} `json:"repository"`
|
||||
Commits []struct {
|
||||
Modified []string `json:"modified"`
|
||||
Added []string `json:"added"`
|
||||
Removed []string `json:"removed"`
|
||||
} `json:"commits"`
|
||||
}
|
||||
|
||||
func (s *Server) handleWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "read body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validar signature si hay secret configurado
|
||||
secret := os.Getenv("DEPLOY_WEBHOOK_SECRET")
|
||||
if secret != "" {
|
||||
sig := r.Header.Get("X-Gitea-Signature")
|
||||
if !verifySignature(body, sig, secret) {
|
||||
http.Error(w, "invalid signature", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var payload giteaPushPayload
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
http.Error(w, "parse payload", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Detectar qué apps fueron afectadas por los cambios
|
||||
apps := s.detectAffectedApps(payload)
|
||||
if len(apps) == 0 {
|
||||
// Intentar match por nombre de repo
|
||||
targets, _ := s.store.GetTargets(payload.Repository.Name)
|
||||
if len(targets) > 0 {
|
||||
apps = append(apps, payload.Repository.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(apps) == 0 {
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "no matching targets"})
|
||||
return
|
||||
}
|
||||
|
||||
// Deploy cada app afectada
|
||||
results := make(map[string]string)
|
||||
for _, app := range apps {
|
||||
targets, _ := s.store.GetTargets(app)
|
||||
for _, t := range targets {
|
||||
if err := s.deployer.Deploy(t, "webhook"); err != nil {
|
||||
results[app+"@"+t.Host] = "failed: " + err.Error()
|
||||
} else {
|
||||
results[app+"@"+t.Host] = "deployed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(results)
|
||||
}
|
||||
|
||||
func (s *Server) detectAffectedApps(payload giteaPushPayload) []string {
|
||||
seen := make(map[string]bool)
|
||||
var apps []string
|
||||
|
||||
for _, commit := range payload.Commits {
|
||||
allFiles := append(append(commit.Modified, commit.Added...), commit.Removed...)
|
||||
for _, f := range allFiles {
|
||||
// Detectar apps/ paths
|
||||
if strings.HasPrefix(f, "apps/") {
|
||||
parts := strings.SplitN(f, "/", 3)
|
||||
if len(parts) >= 2 && !seen[parts[1]] {
|
||||
seen[parts[1]] = true
|
||||
apps = append(apps, parts[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
||||
func (s *Server) handleListTargets(w http.ResponseWriter, r *http.Request) {
|
||||
targets, err := s.store.ListTargets()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(targets)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetTarget(w http.ResponseWriter, r *http.Request) {
|
||||
app := r.PathValue("app")
|
||||
targets, err := s.store.GetTargets(app)
|
||||
if err != nil || len(targets) == 0 {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(targets)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeploy(w http.ResponseWriter, r *http.Request) {
|
||||
app := r.PathValue("app")
|
||||
targets, err := s.store.GetTargets(app)
|
||||
if err != nil || len(targets) == 0 {
|
||||
http.Error(w, "no targets for "+app, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
results := make(map[string]string)
|
||||
for _, t := range targets {
|
||||
if err := s.deployer.Deploy(t, "api"); err != nil {
|
||||
results[t.Host] = "failed: " + err.Error()
|
||||
} else {
|
||||
results[t.Host] = "deployed"
|
||||
}
|
||||
}
|
||||
json.NewEncoder(w).Encode(results)
|
||||
}
|
||||
|
||||
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
app := r.PathValue("app")
|
||||
targets, _ := s.store.GetTargets(app)
|
||||
|
||||
type statusResult struct {
|
||||
Host string `json:"host"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
var results []statusResult
|
||||
for _, t := range targets {
|
||||
out, _ := s.deployer.Status(t)
|
||||
results = append(results, statusResult{Host: t.Host, Status: out})
|
||||
}
|
||||
json.NewEncoder(w).Encode(results)
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, `{"status":"ok"}`)
|
||||
}
|
||||
|
||||
func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) {
|
||||
app := r.PathValue("app")
|
||||
logs, err := s.store.RecentLogs(app, 20)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(logs)
|
||||
}
|
||||
|
||||
func verifySignature(body []byte, signature, secret string) bool {
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write(body)
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
return hmac.Equal([]byte(expected), []byte(signature))
|
||||
}
|
||||
|
||||
// registryRootFromExe intenta derivar la raíz del registry desde el ejecutable.
|
||||
func registryRootFromExe() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
// apps/deploy_server/deploy_server → raíz es ../../
|
||||
return filepath.Dir(filepath.Dir(filepath.Dir(exe)))
|
||||
}
|
||||
Reference in New Issue
Block a user