package scst import ( "fmt" "net/http" "strings" "github.com/atlasos/calypso/internal/common/database" "github.com/atlasos/calypso/internal/common/logger" "github.com/atlasos/calypso/internal/tasks" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" ) // Handler handles SCST-related API requests type Handler struct { service *Service taskEngine *tasks.Engine db *database.DB logger *logger.Logger } // NewHandler creates a new SCST handler func NewHandler(db *database.DB, log *logger.Logger) *Handler { return &Handler{ service: NewService(db, log), taskEngine: tasks.NewEngine(db, log), db: db, logger: log, } } // ListTargets lists all SCST targets func (h *Handler) ListTargets(c *gin.Context) { targets, err := h.service.ListTargets(c.Request.Context()) if err != nil { h.logger.Error("Failed to list targets", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list targets"}) return } // Ensure we return an empty array instead of null if targets == nil { targets = []Target{} } c.JSON(http.StatusOK, gin.H{"targets": targets}) } // GetTarget retrieves a target by ID func (h *Handler) GetTarget(c *gin.Context) { targetID := c.Param("id") target, err := h.service.GetTarget(c.Request.Context(), targetID) if err != nil { if err.Error() == "target not found" { c.JSON(http.StatusNotFound, gin.H{"error": "target not found"}) return } h.logger.Error("Failed to get target", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get target"}) return } // Get LUNs luns, err := h.service.GetTargetLUNs(c.Request.Context(), targetID) if err != nil { h.logger.Warn("Failed to get LUNs", "target_id", targetID, "error", err) // Return empty array instead of nil luns = []LUN{} } // Get initiator groups groups, err2 := h.service.GetTargetInitiatorGroups(c.Request.Context(), targetID) if err2 != nil { h.logger.Warn("Failed to get initiator groups", "target_id", targetID, "error", err2) groups = []InitiatorGroup{} } c.JSON(http.StatusOK, gin.H{ "target": target, "luns": luns, "initiator_groups": groups, }) } // CreateTargetRequest represents a target creation request type CreateTargetRequest struct { IQN string `json:"iqn" binding:"required"` TargetType string `json:"target_type" binding:"required"` Name string `json:"name" binding:"required"` Description string `json:"description"` SingleInitiatorOnly bool `json:"single_initiator_only"` } // CreateTarget creates a new SCST target func (h *Handler) CreateTarget(c *gin.Context) { var req CreateTargetRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } userID, _ := c.Get("user_id") target := &Target{ IQN: req.IQN, TargetType: req.TargetType, Name: req.Name, Description: req.Description, IsActive: true, SingleInitiatorOnly: req.SingleInitiatorOnly || req.TargetType == "vtl" || req.TargetType == "physical_tape", CreatedBy: userID.(string), } if err := h.service.CreateTarget(c.Request.Context(), target); err != nil { h.logger.Error("Failed to create target", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Set alias to name for frontend compatibility (same as ListTargets) target.Alias = target.Name // LUNCount will be 0 for newly created target target.LUNCount = 0 c.JSON(http.StatusCreated, target) } // AddLUNRequest represents a LUN addition request type AddLUNRequest struct { DeviceName string `json:"device_name" binding:"required"` DevicePath string `json:"device_path" binding:"required"` LUNNumber int `json:"lun_number"` // Note: cannot use binding:"required" for int as 0 is valid HandlerType string `json:"handler_type" binding:"required"` } // AddLUN adds a LUN to a target func (h *Handler) AddLUN(c *gin.Context) { targetID := c.Param("id") target, err := h.service.GetTarget(c.Request.Context(), targetID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "target not found"}) return } var req AddLUNRequest if err := c.ShouldBindJSON(&req); err != nil { h.logger.Error("Failed to bind AddLUN request", "error", err) // Provide more detailed error message if validationErr, ok := err.(validator.ValidationErrors); ok { var errorMessages []string for _, fieldErr := range validationErr { errorMessages = append(errorMessages, fmt.Sprintf("%s is required", fieldErr.Field())) } c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("validation failed: %s", strings.Join(errorMessages, ", "))}) } else { // Extract error message without full struct name errMsg := err.Error() if idx := strings.Index(errMsg, "Key: '"); idx >= 0 { // Extract field name from error message fieldStart := idx + 6 // Length of "Key: '" if fieldEnd := strings.Index(errMsg[fieldStart:], "'"); fieldEnd >= 0 { fieldName := errMsg[fieldStart : fieldStart+fieldEnd] c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid or missing field: %s", fieldName)}) } else { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request format"}) } } else { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid request: %v", err)}) } } return } // Validate required fields (additional check in case binding doesn't catch it) if req.DeviceName == "" || req.DevicePath == "" || req.HandlerType == "" { h.logger.Error("Missing required fields in AddLUN request", "device_name", req.DeviceName, "device_path", req.DevicePath, "handler_type", req.HandlerType) c.JSON(http.StatusBadRequest, gin.H{"error": "device_name, device_path, and handler_type are required"}) return } // Validate LUN number range if req.LUNNumber < 0 || req.LUNNumber > 255 { c.JSON(http.StatusBadRequest, gin.H{"error": "lun_number must be between 0 and 255"}) return } if err := h.service.AddLUN(c.Request.Context(), target.IQN, req.DeviceName, req.DevicePath, req.LUNNumber, req.HandlerType); err != nil { h.logger.Error("Failed to add LUN", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "LUN added successfully"}) } // RemoveLUN removes a LUN from a target func (h *Handler) RemoveLUN(c *gin.Context) { targetID := c.Param("id") lunID := c.Param("lunId") // Get target target, err := h.service.GetTarget(c.Request.Context(), targetID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "target not found"}) return } // Get LUN to get the LUN number var lunNumber int err = h.db.QueryRowContext(c.Request.Context(), "SELECT lun_number FROM scst_luns WHERE id = $1 AND target_id = $2", lunID, targetID, ).Scan(&lunNumber) if err != nil { if strings.Contains(err.Error(), "no rows") { // LUN already deleted from database - check if it still exists in SCST // Try to get LUN number from URL or try common LUN numbers // For now, return success since it's already deleted (idempotent) h.logger.Info("LUN not found in database, may already be deleted", "lun_id", lunID, "target_id", targetID) c.JSON(http.StatusOK, gin.H{"message": "LUN already removed or not found"}) return } h.logger.Error("Failed to get LUN", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get LUN"}) return } // Remove LUN if err := h.service.RemoveLUN(c.Request.Context(), target.IQN, lunNumber); err != nil { h.logger.Error("Failed to remove LUN", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "LUN removed successfully"}) } // AddInitiatorRequest represents an initiator addition request type AddInitiatorRequest struct { InitiatorIQN string `json:"initiator_iqn" binding:"required"` } // AddInitiator adds an initiator to a target func (h *Handler) AddInitiator(c *gin.Context) { targetID := c.Param("id") target, err := h.service.GetTarget(c.Request.Context(), targetID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "target not found"}) return } var req AddInitiatorRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } if err := h.service.AddInitiator(c.Request.Context(), target.IQN, req.InitiatorIQN); err != nil { h.logger.Error("Failed to add initiator", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Initiator added successfully"}) } // AddInitiatorToGroupRequest represents a request to add an initiator to a group type AddInitiatorToGroupRequest struct { InitiatorIQN string `json:"initiator_iqn" binding:"required"` } // AddInitiatorToGroup adds an initiator to a specific group func (h *Handler) AddInitiatorToGroup(c *gin.Context) { groupID := c.Param("id") var req AddInitiatorToGroupRequest if err := c.ShouldBindJSON(&req); err != nil { validationErrors := make(map[string]string) if ve, ok := err.(validator.ValidationErrors); ok { for _, fe := range ve { field := strings.ToLower(fe.Field()) validationErrors[field] = fmt.Sprintf("Field '%s' is required", field) } } c.JSON(http.StatusBadRequest, gin.H{ "error": "invalid request", "validation_errors": validationErrors, }) return } err := h.service.AddInitiatorToGroup(c.Request.Context(), groupID, req.InitiatorIQN) if err != nil { if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "single initiator only") { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } h.logger.Error("Failed to add initiator to group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add initiator to group"}) return } c.JSON(http.StatusOK, gin.H{"message": "Initiator added to group successfully"}) } // ListAllInitiators lists all initiators across all targets func (h *Handler) ListAllInitiators(c *gin.Context) { initiators, err := h.service.ListAllInitiators(c.Request.Context()) if err != nil { h.logger.Error("Failed to list initiators", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list initiators"}) return } if initiators == nil { initiators = []InitiatorWithTarget{} } c.JSON(http.StatusOK, gin.H{"initiators": initiators}) } // RemoveInitiator removes an initiator func (h *Handler) RemoveInitiator(c *gin.Context) { initiatorID := c.Param("id") if err := h.service.RemoveInitiator(c.Request.Context(), initiatorID); err != nil { if err.Error() == "initiator not found" { c.JSON(http.StatusNotFound, gin.H{"error": "initiator not found"}) return } h.logger.Error("Failed to remove initiator", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Initiator removed successfully"}) } // GetInitiator retrieves an initiator by ID func (h *Handler) GetInitiator(c *gin.Context) { initiatorID := c.Param("id") initiator, err := h.service.GetInitiator(c.Request.Context(), initiatorID) if err != nil { if err.Error() == "initiator not found" { c.JSON(http.StatusNotFound, gin.H{"error": "initiator not found"}) return } h.logger.Error("Failed to get initiator", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get initiator"}) return } c.JSON(http.StatusOK, initiator) } // ListExtents lists all device extents func (h *Handler) ListExtents(c *gin.Context) { extents, err := h.service.ListExtents(c.Request.Context()) if err != nil { h.logger.Error("Failed to list extents", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list extents"}) return } if extents == nil { extents = []Extent{} } c.JSON(http.StatusOK, gin.H{"extents": extents}) } // CreateExtentRequest represents a request to create an extent type CreateExtentRequest struct { DeviceName string `json:"device_name" binding:"required"` DevicePath string `json:"device_path" binding:"required"` HandlerType string `json:"handler_type" binding:"required"` } // CreateExtent creates a new device extent func (h *Handler) CreateExtent(c *gin.Context) { var req CreateExtentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } if err := h.service.CreateExtent(c.Request.Context(), req.DeviceName, req.DevicePath, req.HandlerType); err != nil { h.logger.Error("Failed to create extent", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"message": "Extent created successfully"}) } // DeleteExtent deletes a device extent func (h *Handler) DeleteExtent(c *gin.Context) { deviceName := c.Param("device") if err := h.service.DeleteExtent(c.Request.Context(), deviceName); err != nil { h.logger.Error("Failed to delete extent", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Extent deleted successfully"}) } // ApplyConfig applies SCST configuration func (h *Handler) ApplyConfig(c *gin.Context) { userID, _ := c.Get("user_id") // Create async task taskID, err := h.taskEngine.CreateTask(c.Request.Context(), tasks.TaskTypeApplySCST, userID.(string), map[string]interface{}{ "operation": "apply_scst_config", }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create task"}) return } // Run apply in background go func() { ctx := c.Request.Context() h.taskEngine.StartTask(ctx, taskID) h.taskEngine.UpdateProgress(ctx, taskID, 50, "Writing SCST configuration...") configPath := "/etc/calypso/scst/generated.conf" if err := h.service.WriteConfig(ctx, configPath); err != nil { h.taskEngine.FailTask(ctx, taskID, err.Error()) return } h.taskEngine.UpdateProgress(ctx, taskID, 100, "SCST configuration applied") h.taskEngine.CompleteTask(ctx, taskID, "SCST configuration applied successfully") }() c.JSON(http.StatusAccepted, gin.H{"task_id": taskID}) } // ListHandlers lists available SCST handlers func (h *Handler) ListHandlers(c *gin.Context) { handlers, err := h.service.DetectHandlers(c.Request.Context()) if err != nil { h.logger.Error("Failed to list handlers", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list handlers"}) return } c.JSON(http.StatusOK, gin.H{"handlers": handlers}) } // ListPortals lists all iSCSI portals func (h *Handler) ListPortals(c *gin.Context) { portals, err := h.service.ListPortals(c.Request.Context()) if err != nil { h.logger.Error("Failed to list portals", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list portals"}) return } // Ensure we return an empty array instead of null if portals == nil { portals = []Portal{} } c.JSON(http.StatusOK, gin.H{"portals": portals}) } // CreatePortal creates a new portal func (h *Handler) CreatePortal(c *gin.Context) { var portal Portal if err := c.ShouldBindJSON(&portal); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } if err := h.service.CreatePortal(c.Request.Context(), &portal); err != nil { h.logger.Error("Failed to create portal", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, portal) } // UpdatePortal updates a portal func (h *Handler) UpdatePortal(c *gin.Context) { id := c.Param("id") var portal Portal if err := c.ShouldBindJSON(&portal); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } if err := h.service.UpdatePortal(c.Request.Context(), id, &portal); err != nil { if err.Error() == "portal not found" { c.JSON(http.StatusNotFound, gin.H{"error": "portal not found"}) return } h.logger.Error("Failed to update portal", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, portal) } // EnableTarget enables a target func (h *Handler) EnableTarget(c *gin.Context) { targetID := c.Param("id") target, err := h.service.GetTarget(c.Request.Context(), targetID) if err != nil { if err.Error() == "target not found" { c.JSON(http.StatusNotFound, gin.H{"error": "target not found"}) return } h.logger.Error("Failed to get target", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get target"}) return } if err := h.service.EnableTarget(c.Request.Context(), target.IQN); err != nil { h.logger.Error("Failed to enable target", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Target enabled successfully"}) } // DisableTarget disables a target func (h *Handler) DisableTarget(c *gin.Context) { targetID := c.Param("id") target, err := h.service.GetTarget(c.Request.Context(), targetID) if err != nil { if err.Error() == "target not found" { c.JSON(http.StatusNotFound, gin.H{"error": "target not found"}) return } h.logger.Error("Failed to get target", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get target"}) return } if err := h.service.DisableTarget(c.Request.Context(), target.IQN); err != nil { h.logger.Error("Failed to disable target", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Target disabled successfully"}) } // DeleteTarget deletes a target func (h *Handler) DeleteTarget(c *gin.Context) { targetID := c.Param("id") if err := h.service.DeleteTarget(c.Request.Context(), targetID); err != nil { if err.Error() == "target not found" { c.JSON(http.StatusNotFound, gin.H{"error": "target not found"}) return } h.logger.Error("Failed to delete target", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Target deleted successfully"}) } // DeletePortal deletes a portal func (h *Handler) DeletePortal(c *gin.Context) { id := c.Param("id") if err := h.service.DeletePortal(c.Request.Context(), id); err != nil { if err.Error() == "portal not found" { c.JSON(http.StatusNotFound, gin.H{"error": "portal not found"}) return } h.logger.Error("Failed to delete portal", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Portal deleted successfully"}) } // GetPortal retrieves a portal by ID func (h *Handler) GetPortal(c *gin.Context) { id := c.Param("id") portal, err := h.service.GetPortal(c.Request.Context(), id) if err != nil { if err.Error() == "portal not found" { c.JSON(http.StatusNotFound, gin.H{"error": "portal not found"}) return } h.logger.Error("Failed to get portal", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get portal"}) return } c.JSON(http.StatusOK, portal) } // CreateInitiatorGroupRequest represents a request to create an initiator group type CreateInitiatorGroupRequest struct { TargetID string `json:"target_id" binding:"required"` GroupName string `json:"group_name" binding:"required"` } // CreateInitiatorGroup creates a new initiator group func (h *Handler) CreateInitiatorGroup(c *gin.Context) { var req CreateInitiatorGroupRequest if err := c.ShouldBindJSON(&req); err != nil { validationErrors := make(map[string]string) if ve, ok := err.(validator.ValidationErrors); ok { for _, fe := range ve { field := strings.ToLower(fe.Field()) validationErrors[field] = fmt.Sprintf("Field '%s' is required", field) } } c.JSON(http.StatusBadRequest, gin.H{ "error": "invalid request", "validation_errors": validationErrors, }) return } group, err := h.service.CreateInitiatorGroup(c.Request.Context(), req.TargetID, req.GroupName) if err != nil { if strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "not found") { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } h.logger.Error("Failed to create initiator group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create initiator group"}) return } c.JSON(http.StatusOK, group) } // UpdateInitiatorGroupRequest represents a request to update an initiator group type UpdateInitiatorGroupRequest struct { GroupName string `json:"group_name" binding:"required"` } // UpdateInitiatorGroup updates an initiator group func (h *Handler) UpdateInitiatorGroup(c *gin.Context) { groupID := c.Param("id") var req UpdateInitiatorGroupRequest if err := c.ShouldBindJSON(&req); err != nil { validationErrors := make(map[string]string) if ve, ok := err.(validator.ValidationErrors); ok { for _, fe := range ve { field := strings.ToLower(fe.Field()) validationErrors[field] = fmt.Sprintf("Field '%s' is required", field) } } c.JSON(http.StatusBadRequest, gin.H{ "error": "invalid request", "validation_errors": validationErrors, }) return } group, err := h.service.UpdateInitiatorGroup(c.Request.Context(), groupID, req.GroupName) if err != nil { if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "already exists") { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } h.logger.Error("Failed to update initiator group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update initiator group"}) return } c.JSON(http.StatusOK, group) } // DeleteInitiatorGroup deletes an initiator group func (h *Handler) DeleteInitiatorGroup(c *gin.Context) { groupID := c.Param("id") err := h.service.DeleteInitiatorGroup(c.Request.Context(), groupID) if err != nil { if strings.Contains(err.Error(), "not found") { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } if strings.Contains(err.Error(), "cannot delete") || strings.Contains(err.Error(), "contains") { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } h.logger.Error("Failed to delete initiator group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete initiator group"}) return } c.JSON(http.StatusOK, gin.H{"message": "initiator group deleted successfully"}) } // GetInitiatorGroup retrieves an initiator group by ID func (h *Handler) GetInitiatorGroup(c *gin.Context) { groupID := c.Param("id") group, err := h.service.GetInitiatorGroup(c.Request.Context(), groupID) if err != nil { if strings.Contains(err.Error(), "not found") { c.JSON(http.StatusNotFound, gin.H{"error": "initiator group not found"}) return } h.logger.Error("Failed to get initiator group", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get initiator group"}) return } c.JSON(http.StatusOK, group) } // ListAllInitiatorGroups lists all initiator groups func (h *Handler) ListAllInitiatorGroups(c *gin.Context) { groups, err := h.service.ListAllInitiatorGroups(c.Request.Context()) if err != nil { h.logger.Error("Failed to list initiator groups", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list initiator groups"}) return } if groups == nil { groups = []InitiatorGroup{} } c.JSON(http.StatusOK, gin.H{"groups": groups}) } // GetConfigFile reads the SCST configuration file content func (h *Handler) GetConfigFile(c *gin.Context) { configPath := c.DefaultQuery("path", "/etc/scst.conf") content, err := h.service.ReadConfigFile(c.Request.Context(), configPath) if err != nil { h.logger.Error("Failed to read config file", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "content": content, "path": configPath, }) } // UpdateConfigFile writes content to SCST configuration file func (h *Handler) UpdateConfigFile(c *gin.Context) { var req struct { Content string `json:"content" binding:"required"` Path string `json:"path"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } configPath := req.Path if configPath == "" { configPath = "/etc/scst.conf" } if err := h.service.WriteConfigFile(c.Request.Context(), configPath, req.Content); err != nil { h.logger.Error("Failed to write config file", "error", err) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "message": "Configuration file updated successfully", "path": configPath, }) }