diff --git a/apps/api-server/internal/handlers/platform.go b/apps/api-server/internal/handlers/platform.go index a5c1706..8a19113 100644 --- a/apps/api-server/internal/handlers/platform.go +++ b/apps/api-server/internal/handlers/platform.go @@ -1,15 +1,22 @@ +// Package handlers hosts HTTP handler types split by domain: +// +// platform_services.go - ServicesHandler (Kubernetes services) +// platform_monitoring.go - MonitoringHandler (Prometheus metrics) +// platform_deployments.go - DeploymentsHandler (deployment lifecycle) +// platform_plugins.go - PluginsHandler (plugin lifecycle) +// platform_automation.go - AutomationHandler (automations, schedules) +// platform_config.go - ConfigHandler (runtime config) +// platform_chaos.go - ChaosHandler (chaos experiments) +// platform_billing.go - BillingHandler (invoices, payments) +// +// This file holds only the shared ErrorResponse envelope and small helpers. package handlers import ( - "net/http" - "strconv" +"net/http" +"strconv" - "github.com/gin-gonic/gin" - "gorm.io/gorm" - - "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" - "github.com/nutcas3/telecom-platform/apps/api-server/internal/monitoring" - "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" +"github.com/gin-gonic/gin" ) // ErrorResponse is a shared error envelope used across platform handlers. @@ -31,7 +38,7 @@ func notFound(c *gin.Context, err error) { c.JSON(http.StatusNotFound, ErrorResponse{Error: "Not found", Details: err.Error(), Code: "NOT_FOUND"}) } -// parseUint helper +// parseUintParam parses a uint route parameter and writes a 400 on failure. func parseUintParam(c *gin.Context, name string) (uint, bool) { v, err := strconv.ParseUint(c.Param(name), 10, 32) if err != nil { @@ -53,656 +60,3 @@ func parseBoolQuery(c *gin.Context, key string) *bool { } return &b } - -type ServicesHandler struct { - k8s *services.KubernetesService -} - -func NewServicesHandler(k8s *services.KubernetesService) *ServicesHandler { - return &ServicesHandler{k8s: k8s} -} - -func (h *ServicesHandler) List(c *gin.Context) { - if h.k8s == nil { - c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) - return - } - deps, err := h.k8s.ListDeployments(c.Request.Context()) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"services": deps, "total": len(deps), "namespace": h.k8s.Namespace()}) -} - -func (h *ServicesHandler) Get(c *gin.Context) { - if h.k8s == nil { - c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) - return - } - d, err := h.k8s.GetDeployment(c.Request.Context(), c.Param("id")) - if err != nil { - notFound(c, err) - return - } - c.JSON(http.StatusOK, d) -} - -func (h *ServicesHandler) Restart(c *gin.Context) { - if h.k8s == nil { - c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) - return - } - if err := h.k8s.RestartDeployment(c.Request.Context(), c.Param("id")); err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"message": "restart triggered", "service": c.Param("id")}) -} - -func (h *ServicesHandler) Start(c *gin.Context) { - if h.k8s == nil { - c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) - return - } - // Starting = scaling to >=1 replica (default 1). - if err := h.k8s.ScaleDeployment(c.Request.Context(), c.Param("id"), 1); err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"message": "service started", "service": c.Param("id")}) -} - -func (h *ServicesHandler) Stop(c *gin.Context) { - if h.k8s == nil { - c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) - return - } - if err := h.k8s.ScaleDeployment(c.Request.Context(), c.Param("id"), 0); err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"message": "service stopped", "service": c.Param("id")}) -} - -func (h *ServicesHandler) Scale(c *gin.Context) { - if h.k8s == nil { - c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) - return - } - var req struct { - Replicas int32 `json:"replicas" binding:"required,min=0,max=100"` - } - if err := c.ShouldBindJSON(&req); err != nil { - badRequest(c, err) - return - } - if err := h.k8s.ScaleDeployment(c.Request.Context(), c.Param("id"), req.Replicas); err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"message": "scaled", "service": c.Param("id"), "replicas": req.Replicas}) -} - -func (h *ServicesHandler) Logs(c *gin.Context) { - if h.k8s == nil { - c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) - return - } - lines, _ := strconv.Atoi(c.DefaultQuery("lines", "100")) - logs, err := h.k8s.PodLogs(c.Request.Context(), c.Param("id"), lines) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"service": c.Param("id"), "logs": logs}) -} - -func (h *ServicesHandler) Health(c *gin.Context) { - if h.k8s == nil { - c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) - return - } - d, err := h.k8s.GetDeployment(c.Request.Context(), c.Param("id")) - if err != nil { - notFound(c, err) - return - } - status := "healthy" - if d.ReadyReplicas < d.Replicas { - status = "degraded" - } - if d.ReadyReplicas == 0 { - status = "unhealthy" - } - c.JSON(http.StatusOK, gin.H{ - "service": c.Param("id"), - "status": status, - "replicas_desired": d.Replicas, - "replicas_ready": d.ReadyReplicas, - "replicas_available": d.AvailableReplicas, - }) -} - -func (h *ServicesHandler) PodStatus(c *gin.Context) { - if h.k8s == nil { - c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) - return - } - status, err := h.k8s.GetPodStatus(c.Request.Context(), c.Param("id")) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, status) -} - -func (h *ServicesHandler) Events(c *gin.Context) { - if h.k8s == nil { - c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) - return - } - limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) - events, err := h.k8s.GetEvents(c.Request.Context(), c.Param("id"), limit) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"events": events, "total": len(events)}) -} - -type MonitoringHandler struct { - prom *monitoring.PrometheusService -} - -func NewMonitoringHandler(prom *monitoring.PrometheusService) *MonitoringHandler { - return &MonitoringHandler{prom: prom} -} - -func (h *MonitoringHandler) Metrics(c *gin.Context) { - if h.prom == nil { - c.JSON(http.StatusServiceUnavailable, gin.H{"error": "monitoring service not available"}) - return - } - q := c.DefaultQuery("query", "up") - samples, err := h.prom.Query(c.Request.Context(), q) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"query": q, "samples": samples}) -} - -func (h *MonitoringHandler) Alerts(c *gin.Context) { - if h.prom == nil { - c.JSON(http.StatusServiceUnavailable, gin.H{"error": "monitoring service not available"}) - return - } - alerts, err := h.prom.Alerts(c.Request.Context()) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"alerts": alerts}) -} - -func (h *MonitoringHandler) Health(c *gin.Context) { - if h.prom == nil { - c.JSON(http.StatusServiceUnavailable, gin.H{ - "status": "degraded", - "prometheus_available": false, - "error": "monitoring service not available", - }) - return - } - available := h.prom.Available(c.Request.Context()) - status := "healthy" - if !available { - status = "degraded" - } - c.JSON(http.StatusOK, gin.H{ - "status": status, - "prometheus_available": available, - }) -} - -func (h *MonitoringHandler) Logs(c *gin.Context) { - // Logs are aggregated in Elasticsearch; expose a stub that points callers - // to /v1/services/:id/logs for live pod logs. - c.JSON(http.StatusOK, gin.H{ - "message": "Use /v1/services/:id/logs for live pod logs; aggregated logs are in Kibana", - }) -} - -type DeploymentsHandler struct { - svc *services.DeploymentService -} - -func NewDeploymentsHandler(svc *services.DeploymentService) *DeploymentsHandler { - return &DeploymentsHandler{svc: svc} -} - -func (h *DeploymentsHandler) Status(c *gin.Context) { - f := services.DeploymentFilter{ - Service: c.Query("service"), - Environment: c.Query("environment"), - Status: "in_progress", - Limit: 50, - } - items, total, err := h.svc.List(c.Request.Context(), f) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"deployments": items, "total": total}) -} - -func (h *DeploymentsHandler) Start(c *gin.Context) { - var in services.StartDeploymentInput - if err := c.ShouldBindJSON(&in); err != nil { - badRequest(c, err) - return - } - d, err := h.svc.Start(c.Request.Context(), in) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusCreated, d) -} - -func (h *DeploymentsHandler) Rollback(c *gin.Context) { - var in services.RollbackInput - if err := c.ShouldBindJSON(&in); err != nil { - badRequest(c, err) - return - } - d, err := h.svc.Rollback(c.Request.Context(), in) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusCreated, d) -} - -func (h *DeploymentsHandler) History(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - if pageSize > 100 { - pageSize = 100 - } - f := services.DeploymentFilter{ - Service: c.Query("service"), - Environment: c.Query("environment"), - Status: c.Query("status"), - Limit: pageSize, - Offset: (page - 1) * pageSize, - } - items, total, err := h.svc.List(c.Request.Context(), f) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{ - "deployments": items, - "total": total, - "page": page, - "page_size": pageSize, - }) -} - -type PluginsHandler struct { - svc *services.PluginService -} - -func NewPluginsHandler(svc *services.PluginService) *PluginsHandler { - return &PluginsHandler{svc: svc} -} - -func (h *PluginsHandler) List(c *gin.Context) { - items, err := h.svc.List(c.Request.Context(), services.PluginFilter{ - Enabled: parseBoolQuery(c, "enabled"), - Type: c.Query("type"), - Category: c.Query("category"), - }) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"plugins": items, "total": len(items)}) -} - -func (h *PluginsHandler) Get(c *gin.Context) { - id, ok := parseUintParam(c, "id") - if !ok { - return - } - - plugin, err := h.svc.Get(c.Request.Context(), id) - if err != nil { - notFound(c, err) - return - } - - c.JSON(http.StatusOK, plugin) -} - -func (h *PluginsHandler) Install(c *gin.Context) { - var in services.InstallPluginInput - if err := c.ShouldBindJSON(&in); err != nil { - badRequest(c, err) - return - } - p, err := h.svc.Install(c.Request.Context(), in) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusCreated, p) -} - -func (h *PluginsHandler) Uninstall(c *gin.Context) { - id, ok := parseUintParam(c, "id") - if !ok { - return - } - if err := h.svc.Uninstall(c.Request.Context(), id); err != nil { - notFound(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"message": "plugin uninstalled", "id": id}) -} - -func (h *PluginsHandler) Enable(c *gin.Context) { - id, ok := parseUintParam(c, "id") - if !ok { - return - } - p, err := h.svc.SetEnabled(c.Request.Context(), id, true) - if err != nil { - notFound(c, err) - return - } - c.JSON(http.StatusOK, p) -} - -func (h *PluginsHandler) Disable(c *gin.Context) { - id, ok := parseUintParam(c, "id") - if !ok { - return - } - p, err := h.svc.SetEnabled(c.Request.Context(), id, false) - if err != nil { - notFound(c, err) - return - } - c.JSON(http.StatusOK, p) -} - -type AutomationHandler struct { - svc *services.AutomationService -} - -func NewAutomationHandler(svc *services.AutomationService) *AutomationHandler { - return &AutomationHandler{svc: svc} -} - -func (h *AutomationHandler) List(c *gin.Context) { - items, err := h.svc.List(c.Request.Context(), services.AutomationFilter{ - Enabled: parseBoolQuery(c, "enabled"), - Type: c.Query("type"), - }) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"automations": items, "total": len(items)}) -} - -func (h *AutomationHandler) Create(c *gin.Context) { - var in services.CreateAutomationInput - if err := c.ShouldBindJSON(&in); err != nil { - badRequest(c, err) - return - } - a, err := h.svc.Create(c.Request.Context(), in) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusCreated, a) -} - -func (h *AutomationHandler) Run(c *gin.Context) { - id, ok := parseUintParam(c, "id") - if !ok { - return - } - r, err := h.svc.Run(c.Request.Context(), id) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, r) -} - -func (h *AutomationHandler) Schedule(c *gin.Context) { - id, ok := parseUintParam(c, "id") - if !ok { - return - } - var in services.ScheduleInput - if err := c.ShouldBindJSON(&in); err != nil { - badRequest(c, err) - return - } - a, err := h.svc.UpdateSchedule(c.Request.Context(), id, in) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, a) -} - -func (h *AutomationHandler) Logs(c *gin.Context) { - var automationID uint - if raw := c.Query("automation_id"); raw != "" { - v, err := strconv.ParseUint(raw, 10, 32) - if err != nil { - badRequest(c, err) - return - } - automationID = uint(v) - } - limit, _ := strconv.Atoi(c.DefaultQuery("limit", "100")) - runs, err := h.svc.ListRuns(c.Request.Context(), automationID, limit) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"runs": runs, "total": len(runs)}) -} - -type ConfigHandler struct { - svc *services.ConfigStoreService -} - -func NewConfigHandler(svc *services.ConfigStoreService) *ConfigHandler { - return &ConfigHandler{svc: svc} -} - -func (h *ConfigHandler) Get(c *gin.Context) { - entries, err := h.svc.List(c.Request.Context(), c.Query("section")) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"entries": entries, "total": len(entries)}) -} - -func (h *ConfigHandler) Update(c *gin.Context) { - var in services.UpsertConfigInput - if err := c.ShouldBindJSON(&in); err != nil { - badRequest(c, err) - return - } - entry, err := h.svc.Upsert(c.Request.Context(), in) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, entry) -} - -func (h *ConfigHandler) Validate(c *gin.Context) { - res, err := h.svc.Validate(c.Request.Context(), c.Query("section")) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, res) -} - -type ChaosHandler struct { - svc *services.ChaosService -} - -func NewChaosHandler(svc *services.ChaosService) *ChaosHandler { - return &ChaosHandler{svc: svc} -} - -type runExperimentRequest struct { - Type services.ExperimentType `json:"type" binding:"required"` - Target string `json:"target" binding:"required"` - DurationMS int64 `json:"duration_ms"` - Probability float64 `json:"probability"` - Amount int `json:"amount"` -} - -func (h *ChaosHandler) Run(c *gin.Context) { - var req runExperimentRequest - if err := c.ShouldBindJSON(&req); err != nil { - badRequest(c, err) - return - } - cfg := services.ExperimentConfig{ - Duration: millisToDuration(req.DurationMS), - Probability: req.Probability, - Amount: req.Amount, - Target: req.Target, - } - exp, err := h.svc.RunExperiment(c.Request.Context(), req.Type, cfg) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusCreated, exp) -} - -func (h *ChaosHandler) List(c *gin.Context) { - active := h.svc.GetActiveExperiments() - history, _ := h.svc.GetExperimentHistory() - c.JSON(http.StatusOK, gin.H{ - "active": active, - "history": history, - }) -} - -func (h *ChaosHandler) Status(c *gin.Context) { - id := c.Query("id") - if id == "" { - // No id: return summary of active experiments - active := h.svc.GetActiveExperiments() - c.JSON(http.StatusOK, gin.H{ - "active_count": len(active), - "active": active, - }) - return - } - exp, ok := h.svc.GetExperimentStatus(id) - if !ok { - c.JSON(http.StatusNotFound, ErrorResponse{Error: "experiment not found", Code: "NOT_FOUND"}) - return - } - c.JSON(http.StatusOK, exp) -} - -type BillingHandler struct { - invoice *services.InvoiceService - db *gorm.DB -} - -func NewBillingHandler(invoice *services.InvoiceService, db *gorm.DB) *BillingHandler { - return &BillingHandler{invoice: invoice, db: db} -} - -type generateInvoiceRequest struct { - SubscriberID uint `json:"subscriber_id" binding:"required"` - BillingPeriod string `json:"billing_period" binding:"required"` // e.g. "2024-01" -} - -func (h *BillingHandler) GenerateInvoice(c *gin.Context) { - var req generateInvoiceRequest - if err := c.ShouldBindJSON(&req); err != nil { - badRequest(c, err) - return - } - inv, err := h.invoice.GenerateMonthlyInvoice(c.Request.Context(), req.SubscriberID, req.BillingPeriod) - if err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusCreated, inv) -} - -func (h *BillingHandler) ListInvoices(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - if pageSize > 100 { - pageSize = 100 - } - q := h.db.WithContext(c.Request.Context()).Model(&models.Invoice{}).Preload("LineItems") - if s := c.Query("status"); s != "" { - q = q.Where("status = ?", s) - } - if sid := c.Query("subscriber_id"); sid != "" { - q = q.Where("subscriber_id = ?", sid) - } - var total int64 - if err := q.Count(&total).Error; err != nil { - serverError(c, err) - return - } - var items []models.Invoice - if err := q.Order("created_at DESC").Limit(pageSize).Offset((page - 1) * pageSize).Find(&items).Error; err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"invoices": items, "total": total, "page": page, "page_size": pageSize}) -} - -func (h *BillingHandler) ListPayments(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - if pageSize > 100 { - pageSize = 100 - } - q := h.db.WithContext(c.Request.Context()).Model(&models.Transaction{}) - if s := c.Query("status"); s != "" { - q = q.Where("status = ?", s) - } - if sid := c.Query("subscriber_id"); sid != "" { - q = q.Where("subscriber_id = ?", sid) - } - var total int64 - if err := q.Count(&total).Error; err != nil { - serverError(c, err) - return - } - var items []models.Transaction - if err := q.Order("created_at DESC").Limit(pageSize).Offset((page - 1) * pageSize).Find(&items).Error; err != nil { - serverError(c, err) - return - } - c.JSON(http.StatusOK, gin.H{"payments": items, "total": total, "page": page, "page_size": pageSize}) -} diff --git a/apps/api-server/internal/handlers/platform_automation.go b/apps/api-server/internal/handlers/platform_automation.go new file mode 100644 index 0000000..e405e7b --- /dev/null +++ b/apps/api-server/internal/handlers/platform_automation.go @@ -0,0 +1,96 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" +) + +// AutomationHandler exposes automation CRUD, run, and schedule endpoints. +type AutomationHandler struct { + svc *services.AutomationService +} + +// NewAutomationHandler constructs an AutomationHandler. +func NewAutomationHandler(svc *services.AutomationService) *AutomationHandler { + return &AutomationHandler{svc: svc} +} + +func (h *AutomationHandler) List(c *gin.Context) { + items, err := h.svc.List(c.Request.Context(), services.AutomationFilter{ + Enabled: parseBoolQuery(c, "enabled"), + Type: c.Query("type"), + }) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"automations": items, "total": len(items)}) +} + +func (h *AutomationHandler) Create(c *gin.Context) { + var in services.CreateAutomationInput + if err := c.ShouldBindJSON(&in); err != nil { + badRequest(c, err) + return + } + a, err := h.svc.Create(c.Request.Context(), in) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusCreated, a) +} + +func (h *AutomationHandler) Run(c *gin.Context) { + id, ok := parseUintParam(c, "id") + if !ok { + return + } + r, err := h.svc.Run(c.Request.Context(), id) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, r) +} + +func (h *AutomationHandler) Schedule(c *gin.Context) { + id, ok := parseUintParam(c, "id") + if !ok { + return + } + var in services.ScheduleInput + if err := c.ShouldBindJSON(&in); err != nil { + badRequest(c, err) + return + } + a, err := h.svc.UpdateSchedule(c.Request.Context(), id, in) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, a) +} + +func (h *AutomationHandler) Logs(c *gin.Context) { + var automationID uint + if raw := c.Query("automation_id"); raw != "" { + v, err := strconv.ParseUint(raw, 10, 32) + if err != nil { + badRequest(c, err) + return + } + automationID = uint(v) + } + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "100")) + runs, err := h.svc.ListRuns(c.Request.Context(), automationID, limit) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"runs": runs, "total": len(runs)}) +} diff --git a/apps/api-server/internal/handlers/platform_billing.go b/apps/api-server/internal/handlers/platform_billing.go new file mode 100644 index 0000000..2d80509 --- /dev/null +++ b/apps/api-server/internal/handlers/platform_billing.go @@ -0,0 +1,94 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/models" + "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" +) + +// BillingHandler exposes invoice and payment endpoints for platform admins. +type BillingHandler struct { + invoice *services.InvoiceService + db *gorm.DB +} + +// NewBillingHandler constructs a BillingHandler. +func NewBillingHandler(invoice *services.InvoiceService, db *gorm.DB) *BillingHandler { + return &BillingHandler{invoice: invoice, db: db} +} + +type generateInvoiceRequest struct { + SubscriberID uint `json:"subscriber_id" binding:"required"` + BillingPeriod string `json:"billing_period" binding:"required"` // e.g. "2024-01" +} + +func (h *BillingHandler) GenerateInvoice(c *gin.Context) { + var req generateInvoiceRequest + if err := c.ShouldBindJSON(&req); err != nil { + badRequest(c, err) + return + } + inv, err := h.invoice.GenerateMonthlyInvoice(c.Request.Context(), req.SubscriberID, req.BillingPeriod) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusCreated, inv) +} + +func (h *BillingHandler) ListInvoices(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if pageSize > 100 { + pageSize = 100 + } + q := h.db.WithContext(c.Request.Context()).Model(&models.Invoice{}).Preload("LineItems") + if s := c.Query("status"); s != "" { + q = q.Where("status = ?", s) + } + if sid := c.Query("subscriber_id"); sid != "" { + q = q.Where("subscriber_id = ?", sid) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + serverError(c, err) + return + } + var items []models.Invoice + if err := q.Order("created_at DESC").Limit(pageSize).Offset((page - 1) * pageSize).Find(&items).Error; err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"invoices": items, "total": total, "page": page, "page_size": pageSize}) +} + +func (h *BillingHandler) ListPayments(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if pageSize > 100 { + pageSize = 100 + } + q := h.db.WithContext(c.Request.Context()).Model(&models.Transaction{}) + if s := c.Query("status"); s != "" { + q = q.Where("status = ?", s) + } + if sid := c.Query("subscriber_id"); sid != "" { + q = q.Where("subscriber_id = ?", sid) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + serverError(c, err) + return + } + var items []models.Transaction + if err := q.Order("created_at DESC").Limit(pageSize).Offset((page - 1) * pageSize).Find(&items).Error; err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"payments": items, "total": total, "page": page, "page_size": pageSize}) +} diff --git a/apps/api-server/internal/handlers/platform_chaos.go b/apps/api-server/internal/handlers/platform_chaos.go new file mode 100644 index 0000000..bb54df4 --- /dev/null +++ b/apps/api-server/internal/handlers/platform_chaos.go @@ -0,0 +1,74 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" +) + +// ChaosHandler exposes chaos-engineering experiment endpoints. +type ChaosHandler struct { + svc *services.ChaosService +} + +// NewChaosHandler constructs a ChaosHandler. +func NewChaosHandler(svc *services.ChaosService) *ChaosHandler { + return &ChaosHandler{svc: svc} +} + +type runExperimentRequest struct { + Type services.ExperimentType `json:"type" binding:"required"` + Target string `json:"target" binding:"required"` + DurationMS int64 `json:"duration_ms"` + Probability float64 `json:"probability"` + Amount int `json:"amount"` +} + +func (h *ChaosHandler) Run(c *gin.Context) { + var req runExperimentRequest + if err := c.ShouldBindJSON(&req); err != nil { + badRequest(c, err) + return + } + cfg := services.ExperimentConfig{ + Duration: millisToDuration(req.DurationMS), + Probability: req.Probability, + Amount: req.Amount, + Target: req.Target, + } + exp, err := h.svc.RunExperiment(c.Request.Context(), req.Type, cfg) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusCreated, exp) +} + +func (h *ChaosHandler) List(c *gin.Context) { + active := h.svc.GetActiveExperiments() + history, _ := h.svc.GetExperimentHistory() + c.JSON(http.StatusOK, gin.H{ + "active": active, + "history": history, + }) +} + +func (h *ChaosHandler) Status(c *gin.Context) { + id := c.Query("id") + if id == "" { + active := h.svc.GetActiveExperiments() + c.JSON(http.StatusOK, gin.H{ + "active_count": len(active), + "active": active, + }) + return + } + exp, ok := h.svc.GetExperimentStatus(id) + if !ok { + c.JSON(http.StatusNotFound, ErrorResponse{Error: "experiment not found", Code: "NOT_FOUND"}) + return + } + c.JSON(http.StatusOK, exp) +} diff --git a/apps/api-server/internal/handlers/platform_config.go b/apps/api-server/internal/handlers/platform_config.go new file mode 100644 index 0000000..e0d8f7a --- /dev/null +++ b/apps/api-server/internal/handlers/platform_config.go @@ -0,0 +1,51 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" +) + +// ConfigHandler exposes runtime config read/write/validate endpoints. +type ConfigHandler struct { + svc *services.ConfigStoreService +} + +// NewConfigHandler constructs a ConfigHandler. +func NewConfigHandler(svc *services.ConfigStoreService) *ConfigHandler { + return &ConfigHandler{svc: svc} +} + +func (h *ConfigHandler) Get(c *gin.Context) { + entries, err := h.svc.List(c.Request.Context(), c.Query("section")) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"entries": entries, "total": len(entries)}) +} + +func (h *ConfigHandler) Update(c *gin.Context) { + var in services.UpsertConfigInput + if err := c.ShouldBindJSON(&in); err != nil { + badRequest(c, err) + return + } + entry, err := h.svc.Upsert(c.Request.Context(), in) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, entry) +} + +func (h *ConfigHandler) Validate(c *gin.Context) { + res, err := h.svc.Validate(c.Request.Context(), c.Query("section")) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, res) +} diff --git a/apps/api-server/internal/handlers/platform_deployments.go b/apps/api-server/internal/handlers/platform_deployments.go new file mode 100644 index 0000000..a03f928 --- /dev/null +++ b/apps/api-server/internal/handlers/platform_deployments.go @@ -0,0 +1,89 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" +) + +// DeploymentsHandler exposes deployment lifecycle endpoints. +type DeploymentsHandler struct { + svc *services.DeploymentService +} + +// NewDeploymentsHandler constructs a DeploymentsHandler. +func NewDeploymentsHandler(svc *services.DeploymentService) *DeploymentsHandler { + return &DeploymentsHandler{svc: svc} +} + +func (h *DeploymentsHandler) Status(c *gin.Context) { + f := services.DeploymentFilter{ + Service: c.Query("service"), + Environment: c.Query("environment"), + Status: "in_progress", + Limit: 50, + } + items, total, err := h.svc.List(c.Request.Context(), f) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"deployments": items, "total": total}) +} + +func (h *DeploymentsHandler) Start(c *gin.Context) { + var in services.StartDeploymentInput + if err := c.ShouldBindJSON(&in); err != nil { + badRequest(c, err) + return + } + d, err := h.svc.Start(c.Request.Context(), in) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusCreated, d) +} + +func (h *DeploymentsHandler) Rollback(c *gin.Context) { + var in services.RollbackInput + if err := c.ShouldBindJSON(&in); err != nil { + badRequest(c, err) + return + } + d, err := h.svc.Rollback(c.Request.Context(), in) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusCreated, d) +} + +func (h *DeploymentsHandler) History(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if pageSize > 100 { + pageSize = 100 + } + f := services.DeploymentFilter{ + Service: c.Query("service"), + Environment: c.Query("environment"), + Status: c.Query("status"), + Limit: pageSize, + Offset: (page - 1) * pageSize, + } + items, total, err := h.svc.List(c.Request.Context(), f) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{ + "deployments": items, + "total": total, + "page": page, + "page_size": pageSize, + }) +} diff --git a/apps/api-server/internal/handlers/platform_monitoring.go b/apps/api-server/internal/handlers/platform_monitoring.go new file mode 100644 index 0000000..8426d74 --- /dev/null +++ b/apps/api-server/internal/handlers/platform_monitoring.go @@ -0,0 +1,74 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/monitoring" +) + +// MonitoringHandler exposes Prometheus-backed metrics and alert endpoints. +type MonitoringHandler struct { + prom *monitoring.PrometheusService +} + +// NewMonitoringHandler constructs a MonitoringHandler. +func NewMonitoringHandler(prom *monitoring.PrometheusService) *MonitoringHandler { + return &MonitoringHandler{prom: prom} +} + +func (h *MonitoringHandler) Metrics(c *gin.Context) { + if h.prom == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "monitoring service not available"}) + return + } + q := c.DefaultQuery("query", "up") + samples, err := h.prom.Query(c.Request.Context(), q) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"query": q, "samples": samples}) +} + +func (h *MonitoringHandler) Alerts(c *gin.Context) { + if h.prom == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{"error": "monitoring service not available"}) + return + } + alerts, err := h.prom.Alerts(c.Request.Context()) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"alerts": alerts}) +} + +func (h *MonitoringHandler) Health(c *gin.Context) { + if h.prom == nil { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "status": "degraded", + "prometheus_available": false, + "error": "monitoring service not available", + }) + return + } + available := h.prom.Available(c.Request.Context()) + status := "healthy" + if !available { + status = "degraded" + } + c.JSON(http.StatusOK, gin.H{ + "status": status, + "prometheus_available": available, + }) +} + +func (h *MonitoringHandler) Logs(c *gin.Context) { + // Logs are aggregated in Elasticsearch; expose a stub that points callers + // to /v1/services/:id/logs for live pod logs. + c.JSON(http.StatusOK, gin.H{ + "message": "Use /v1/services/:id/logs for live pod logs; aggregated logs are in Kibana", + }) +} diff --git a/apps/api-server/internal/handlers/platform_plugins.go b/apps/api-server/internal/handlers/platform_plugins.go new file mode 100644 index 0000000..d342c7d --- /dev/null +++ b/apps/api-server/internal/handlers/platform_plugins.go @@ -0,0 +1,97 @@ +package handlers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" +) + +// PluginsHandler exposes plugin install/lifecycle endpoints. +type PluginsHandler struct { + svc *services.PluginService +} + +// NewPluginsHandler constructs a PluginsHandler. +func NewPluginsHandler(svc *services.PluginService) *PluginsHandler { + return &PluginsHandler{svc: svc} +} + +func (h *PluginsHandler) List(c *gin.Context) { + items, err := h.svc.List(c.Request.Context(), services.PluginFilter{ + Enabled: parseBoolQuery(c, "enabled"), + Type: c.Query("type"), + Category: c.Query("category"), + }) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"plugins": items, "total": len(items)}) +} + +func (h *PluginsHandler) Get(c *gin.Context) { + id, ok := parseUintParam(c, "id") + if !ok { + return + } + plugin, err := h.svc.Get(c.Request.Context(), id) + if err != nil { + notFound(c, err) + return + } + c.JSON(http.StatusOK, plugin) +} + +func (h *PluginsHandler) Install(c *gin.Context) { + var in services.InstallPluginInput + if err := c.ShouldBindJSON(&in); err != nil { + badRequest(c, err) + return + } + p, err := h.svc.Install(c.Request.Context(), in) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusCreated, p) +} + +func (h *PluginsHandler) Uninstall(c *gin.Context) { + id, ok := parseUintParam(c, "id") + if !ok { + return + } + if err := h.svc.Uninstall(c.Request.Context(), id); err != nil { + notFound(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"message": "plugin uninstalled", "id": id}) +} + +func (h *PluginsHandler) Enable(c *gin.Context) { + id, ok := parseUintParam(c, "id") + if !ok { + return + } + p, err := h.svc.SetEnabled(c.Request.Context(), id, true) + if err != nil { + notFound(c, err) + return + } + c.JSON(http.StatusOK, p) +} + +func (h *PluginsHandler) Disable(c *gin.Context) { + id, ok := parseUintParam(c, "id") + if !ok { + return + } + p, err := h.svc.SetEnabled(c.Request.Context(), id, false) + if err != nil { + notFound(c, err) + return + } + c.JSON(http.StatusOK, p) +} diff --git a/apps/api-server/internal/handlers/platform_services.go b/apps/api-server/internal/handlers/platform_services.go new file mode 100644 index 0000000..c38844f --- /dev/null +++ b/apps/api-server/internal/handlers/platform_services.go @@ -0,0 +1,168 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/nutcas3/telecom-platform/apps/api-server/internal/services" +) + +// ServicesHandler exposes Kubernetes service/deployment operations. +type ServicesHandler struct { + k8s *services.KubernetesService +} + +// NewServicesHandler constructs a ServicesHandler. +func NewServicesHandler(k8s *services.KubernetesService) *ServicesHandler { + return &ServicesHandler{k8s: k8s} +} + +func (h *ServicesHandler) List(c *gin.Context) { + if h.k8s == nil { + c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) + return + } + deps, err := h.k8s.ListDeployments(c.Request.Context()) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"services": deps, "total": len(deps), "namespace": h.k8s.Namespace()}) +} + +func (h *ServicesHandler) Get(c *gin.Context) { + if h.k8s == nil { + c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) + return + } + d, err := h.k8s.GetDeployment(c.Request.Context(), c.Param("id")) + if err != nil { + notFound(c, err) + return + } + c.JSON(http.StatusOK, d) +} + +func (h *ServicesHandler) Restart(c *gin.Context) { + if h.k8s == nil { + c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) + return + } + if err := h.k8s.RestartDeployment(c.Request.Context(), c.Param("id")); err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"message": "restart triggered", "service": c.Param("id")}) +} + +func (h *ServicesHandler) Start(c *gin.Context) { + if h.k8s == nil { + c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) + return + } + if err := h.k8s.ScaleDeployment(c.Request.Context(), c.Param("id"), 1); err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"message": "service started", "service": c.Param("id")}) +} + +func (h *ServicesHandler) Stop(c *gin.Context) { + if h.k8s == nil { + c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) + return + } + if err := h.k8s.ScaleDeployment(c.Request.Context(), c.Param("id"), 0); err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"message": "service stopped", "service": c.Param("id")}) +} + +func (h *ServicesHandler) Scale(c *gin.Context) { + if h.k8s == nil { + c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) + return + } + var req struct { + Replicas int32 `json:"replicas" binding:"required,min=0,max=100"` + } + if err := c.ShouldBindJSON(&req); err != nil { + badRequest(c, err) + return + } + if err := h.k8s.ScaleDeployment(c.Request.Context(), c.Param("id"), req.Replicas); err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"message": "scaled", "service": c.Param("id"), "replicas": req.Replicas}) +} + +func (h *ServicesHandler) Logs(c *gin.Context) { + if h.k8s == nil { + c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) + return + } + lines, _ := strconv.Atoi(c.DefaultQuery("lines", "100")) + logs, err := h.k8s.PodLogs(c.Request.Context(), c.Param("id"), lines) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"service": c.Param("id"), "logs": logs}) +} + +func (h *ServicesHandler) Health(c *gin.Context) { + if h.k8s == nil { + c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) + return + } + d, err := h.k8s.GetDeployment(c.Request.Context(), c.Param("id")) + if err != nil { + notFound(c, err) + return + } + status := "healthy" + if d.ReadyReplicas < d.Replicas { + status = "degraded" + } + if d.ReadyReplicas == 0 { + status = "unhealthy" + } + c.JSON(http.StatusOK, gin.H{ + "service": c.Param("id"), + "status": status, + "replicas_desired": d.Replicas, + "replicas_ready": d.ReadyReplicas, + "replicas_available": d.AvailableReplicas, + }) +} + +func (h *ServicesHandler) PodStatus(c *gin.Context) { + if h.k8s == nil { + c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) + return + } + status, err := h.k8s.GetPodStatus(c.Request.Context(), c.Param("id")) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, status) +} + +func (h *ServicesHandler) Events(c *gin.Context) { + if h.k8s == nil { + c.JSON(http.StatusServiceUnavailable, ErrorResponse{Error: "Kubernetes not configured", Code: "K8S_UNAVAILABLE"}) + return + } + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50")) + events, err := h.k8s.GetEvents(c.Request.Context(), c.Param("id"), limit) + if err != nil { + serverError(c, err) + return + } + c.JSON(http.StatusOK, gin.H{"events": events, "total": len(events)}) +}