added all files
This commit is contained in:
Executable
+231
@@ -0,0 +1,231 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
)
|
||||
|
||||
// Router handles all /api/* requests.
|
||||
type Router struct {
|
||||
cfg *config.Config
|
||||
stackMgr *stacks.Manager
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewRouter(cfg *config.Config, stackMgr *stacks.Manager, logger *log.Logger) *Router {
|
||||
return &Router{cfg: cfg, stackMgr: stackMgr, logger: logger}
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// ServeHTTP routes /api/* requests.
|
||||
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
path := strings.TrimPrefix(req.URL.Path, "/api")
|
||||
path = strings.TrimSuffix(path, "/")
|
||||
|
||||
switch {
|
||||
// GET /api/stacks
|
||||
case path == "/stacks" && req.Method == http.MethodGet:
|
||||
r.listStacks(w, req)
|
||||
|
||||
// GET /api/stacks/{name}
|
||||
case strings.HasPrefix(path, "/stacks/") && req.Method == http.MethodGet && !hasSubpath(path, "/stacks/"):
|
||||
r.getStack(w, req, trimSegment(path, "/stacks/"))
|
||||
|
||||
// GET /api/stacks/{name}/deploy-fields
|
||||
case hasSuffix(path, "/deploy-fields") && req.Method == http.MethodGet:
|
||||
r.getDeployFields(w, req, extractName(path, "/deploy-fields"))
|
||||
|
||||
// POST /api/stacks/{name}/deploy
|
||||
case hasSuffix(path, "/deploy") && req.Method == http.MethodPost:
|
||||
r.deployStack(w, req, extractName(path, "/deploy"))
|
||||
|
||||
// POST /api/stacks/{name}/start
|
||||
case hasSuffix(path, "/start") && req.Method == http.MethodPost:
|
||||
r.actionStack(w, "start", extractName(path, "/start"))
|
||||
|
||||
// POST /api/stacks/{name}/stop
|
||||
case hasSuffix(path, "/stop") && req.Method == http.MethodPost:
|
||||
r.actionStack(w, "stop", extractName(path, "/stop"))
|
||||
|
||||
// POST /api/stacks/{name}/restart
|
||||
case hasSuffix(path, "/restart") && req.Method == http.MethodPost:
|
||||
r.actionStack(w, "restart", extractName(path, "/restart"))
|
||||
|
||||
// POST /api/stacks/{name}/update
|
||||
case hasSuffix(path, "/update") && req.Method == http.MethodPost:
|
||||
r.actionStack(w, "update", extractName(path, "/update"))
|
||||
|
||||
// GET /api/stacks/{name}/logs
|
||||
case hasSuffix(path, "/logs") && req.Method == http.MethodGet:
|
||||
r.getStackLogs(w, req, extractName(path, "/logs"))
|
||||
|
||||
// GET /api/system/info
|
||||
case path == "/system/info" && req.Method == http.MethodGet:
|
||||
r.systemInfo(w, req)
|
||||
|
||||
default:
|
||||
writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: "endpoint not found"})
|
||||
}
|
||||
}
|
||||
|
||||
// HealthHandler responds to /api/health (no auth required).
|
||||
func (r *Router) HealthHandler(w http.ResponseWriter, req *http.Request) {
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "felhom-controller is healthy"})
|
||||
}
|
||||
|
||||
// --- Stack handlers ---
|
||||
|
||||
func (r *Router) listStacks(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: r.stackMgr.GetStacks()})
|
||||
}
|
||||
|
||||
func (r *Router) getStack(w http.ResponseWriter, _ *http.Request, name string) {
|
||||
stack, ok := r.stackMgr.GetStack(name)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: "stack not found: " + name})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: stack})
|
||||
}
|
||||
|
||||
func (r *Router) getDeployFields(w http.ResponseWriter, _ *http.Request, name string) {
|
||||
meta, appCfg, err := r.stackMgr.GetDeployFields(name)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"metadata": meta,
|
||||
"app_config": appCfg,
|
||||
"domain": r.cfg.Customer.Domain,
|
||||
"logo_url": r.cfg.AppLogoURL(meta.Slug),
|
||||
}
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: data})
|
||||
}
|
||||
|
||||
func (r *Router) deployStack(w http.ResponseWriter, req *http.Request, name string) {
|
||||
r.logger.Printf("[API] Deploy requested for stack: %s", name)
|
||||
|
||||
var body struct {
|
||||
Values map[string]string `json:"values"`
|
||||
}
|
||||
if err := json.NewDecoder(req.Body).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
deployReq := stacks.DeployRequest{
|
||||
StackName: name,
|
||||
Values: body.Values,
|
||||
}
|
||||
|
||||
if err := r.stackMgr.DeployStack(deployReq); err != nil {
|
||||
r.logger.Printf("[API] Deploy failed for %s: %v", name, err)
|
||||
status := http.StatusInternalServerError
|
||||
if strings.Contains(err.Error(), "already deployed") {
|
||||
status = http.StatusConflict
|
||||
}
|
||||
if strings.Contains(err.Error(), "required field") || strings.Contains(err.Error(), "does not exist") {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
writeJSON(w, status, apiResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Stack " + name + " deployed"})
|
||||
}
|
||||
|
||||
func (r *Router) actionStack(w http.ResponseWriter, action, name string) {
|
||||
r.logger.Printf("[API] %s requested for stack: %s", action, name)
|
||||
|
||||
var err error
|
||||
switch action {
|
||||
case "start":
|
||||
err = r.stackMgr.StartStack(name)
|
||||
case "stop":
|
||||
err = r.stackMgr.StopStack(name)
|
||||
case "restart":
|
||||
err = r.stackMgr.RestartStack(name)
|
||||
case "update":
|
||||
err = r.stackMgr.UpdateStack(name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
status := http.StatusInternalServerError
|
||||
if strings.Contains(err.Error(), "protected") {
|
||||
status = http.StatusForbidden
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
writeJSON(w, status, apiResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Stack " + name + " " + action + " completed"})
|
||||
}
|
||||
|
||||
func (r *Router) getStackLogs(w http.ResponseWriter, req *http.Request, name string) {
|
||||
lines := 100
|
||||
if v := req.URL.Query().Get("lines"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||
lines = n
|
||||
}
|
||||
}
|
||||
|
||||
output, err := r.stackMgr.GetLogs(name, lines)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: map[string]string{"logs": output}})
|
||||
}
|
||||
|
||||
func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: map[string]interface{}{
|
||||
"customer_id": r.cfg.Customer.ID,
|
||||
"customer_name": r.cfg.Customer.Name,
|
||||
"domain": r.cfg.Customer.Domain,
|
||||
"backup_enabled": r.cfg.Backup.Enabled,
|
||||
"monitor_enabled": r.cfg.Monitoring.Enabled,
|
||||
}})
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func hasSuffix(path, suffix string) bool { return strings.HasSuffix(path, suffix) }
|
||||
|
||||
func hasSubpath(path, prefix string) bool {
|
||||
rest := strings.TrimPrefix(path, prefix)
|
||||
return strings.Contains(rest, "/")
|
||||
}
|
||||
|
||||
func trimSegment(path, prefix string) string {
|
||||
return strings.TrimPrefix(path, prefix)
|
||||
}
|
||||
|
||||
func extractName(path, suffix string) string {
|
||||
s := strings.TrimPrefix(path, "/stacks/")
|
||||
return strings.TrimSuffix(s, suffix)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
log.Printf("[ERROR] Failed to write JSON response: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user