Files
oneterm/backend/internal/api/controller/storage.go

406 lines
14 KiB
Go

package controller
import (
"errors"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/spf13/cast"
"gorm.io/gorm"
"github.com/veops/oneterm/internal/acl"
"github.com/veops/oneterm/internal/model"
"github.com/veops/oneterm/internal/service"
myErrors "github.com/veops/oneterm/pkg/errors"
)
var storageService = service.DefaultStorageService
// ListStorageConfigs godoc
//
// @Tags storage
// @Summary List all storage configurations
// @Param page_index query int false "page_index"
// @Param page_size query int false "page_size"
// @Param search query string false "search"
// @Param type query string false "storage type filter"
// @Param enabled query string false "enabled filter (true/false)"
// @Param primary query string false "primary filter (true/false)"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.StorageConfig}}
// @Router /storage/configs [get]
func (c *Controller) ListStorageConfigs(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
return
}
db := storageService.BuildQuery(ctx)
doGet[*model.StorageConfig](ctx, false, db, "")
}
// GetStorageConfig godoc
//
// @Tags storage
// @Summary Get storage configuration by ID
// @Param id path int true "Storage ID"
// @Success 200 {object} HttpResponse{data=model.StorageConfig}
// @Router /storage/configs/{id} [get]
func (c *Controller) GetStorageConfig(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
return
}
baseService := service.NewBaseService()
id, err := cast.ToIntE(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
config := &model.StorageConfig{}
if err := baseService.GetById(ctx, id, config); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
ctx.AbortWithError(http.StatusNotFound, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": "storage config not found"}})
return
}
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{Data: config})
}
// CreateStorageConfig godoc
//
// @Tags storage
// @Summary Create a new storage configuration
// @Param config body model.StorageConfig true "Storage configuration"
// @Success 200 {object} HttpResponse{}
// @Router /storage/configs [post]
func (c *Controller) CreateStorageConfig(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
doCreate(ctx, false, &model.StorageConfig{}, "", func(ctx *gin.Context, config *model.StorageConfig) {
// Custom validation for storage config
if err := validateStorageConfig(config); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
// Initialize storage provider after creation
if provider, err := storageService.CreateProvider(config); err == nil {
// Test connection
if err := provider.HealthCheck(ctx); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
}
})
}
// UpdateStorageConfig godoc
//
// @Tags storage
// @Summary Update an existing storage configuration
// @Param id path int true "Storage ID"
// @Param config body model.StorageConfig true "Storage configuration"
// @Success 200 {object} HttpResponse{}
// @Router /storage/configs/{id} [put]
func (c *Controller) UpdateStorageConfig(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
doUpdate(ctx, false, &model.StorageConfig{}, "", func(ctx *gin.Context, config *model.StorageConfig) {
// Custom validation for storage config
if err := validateStorageConfig(config); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
// Test connection after update
if provider, err := storageService.CreateProvider(config); err == nil {
if err := provider.HealthCheck(ctx); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
}
})
// Always refresh providers after update to handle name changes
if !ctx.IsAborted() {
if err := storageService.RefreshProviders(ctx); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
}
}
// DeleteStorageConfig godoc
//
// @Tags storage
// @Summary Delete a storage configuration
// @Param id path int true "Storage ID"
// @Success 200 {object} HttpResponse{}
// @Router /storage/configs/{id} [delete]
func (c *Controller) DeleteStorageConfig(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
doDelete(ctx, false, &model.StorageConfig{}, "", func(ctx *gin.Context, id int) {
// Custom validation: check if it's the primary storage
config := &model.StorageConfig{}
baseService := service.NewBaseService()
if err := baseService.GetById(ctx, id, config); err == nil && config.IsPrimary {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "cannot delete primary storage"}})
return
}
})
}
// TestStorageConnection godoc
//
// @Tags storage
// @Summary Test storage connection
// @Param config body model.StorageConfig true "Storage configuration to test"
// @Success 200 {object} HttpResponse{}
// @Router /storage/test-connection [post]
func (c *Controller) TestStorageConnection(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
config := &model.StorageConfig{}
if err := ctx.ShouldBindBodyWithJSON(config); err != nil {
// Provide more detailed error information for JSON parsing issues
errorMsg := fmt.Sprintf("failed to parse request body: %v", err)
if err.Error() == "EOF" {
errorMsg = "request body is empty, please provide storage configuration JSON"
}
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": errorMsg}})
return
}
// Validate required fields
if config.Type == "" {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "storage type is required"}})
return
}
// Create a temporary provider to test connection
provider, err := storageService.CreateProvider(config)
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
// Perform health check
if err := provider.HealthCheck(ctx); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
}
// GetStorageHealth godoc
//
// @Tags storage
// @Summary Get health status of all storage providers
// @Success 200 {object} HttpResponse{data=map[string]any}
// @Router /storage/health [get]
func (c *Controller) GetStorageHealth(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
return
}
healthResults := storageService.HealthCheck(ctx)
// Convert error map to a more API-friendly format
healthStatus := make(map[string]map[string]interface{})
for name, err := range healthResults {
var errorMsg interface{}
if err != nil {
errorMsg = err.Error() // Convert error to string for proper JSON serialization
} else {
errorMsg = nil
}
healthStatus[name] = map[string]interface{}{
"healthy": err == nil,
"error": errorMsg,
}
}
ctx.JSON(http.StatusOK, HttpResponse{Data: healthStatus})
}
// SetPrimaryStorage godoc
//
// @Tags storage
// @Summary Set a storage provider as primary
// @Param id path int true "Storage ID"
// @Success 200 {object} HttpResponse{}
// @Router /storage/configs/{id}/set-primary [put]
func (c *Controller) SetPrimaryStorage(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
id, err := cast.ToIntE(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
// Get current config
baseService := service.NewBaseService()
config := &model.StorageConfig{}
if err := baseService.GetById(ctx, id, config); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
// First, clear all existing primary flags
if err := storageService.ClearAllPrimaryFlags(ctx); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
// Update to set as primary
config.IsPrimary = true
config.UpdaterId = currentUser.GetUid()
if err := storageService.UpdateStorageConfig(ctx, config); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
// Refresh providers after setting new primary to ensure session replay adapter uses the new primary provider
if err := storageService.RefreshProviders(ctx); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
}
// ToggleStorageProvider godoc
//
// @Tags storage
// @Summary Enable or disable a storage provider
// @Param id path int true "Storage ID"
// @Success 200 {object} HttpResponse{}
// @Router /storage/configs/{id}/toggle [put]
func (c *Controller) ToggleStorageProvider(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
id, err := cast.ToIntE(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
// Get current config
baseService := service.NewBaseService()
config := &model.StorageConfig{}
if err := baseService.GetById(ctx, id, config); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
// Toggle enabled status
config.Enabled = !config.Enabled
config.UpdaterId = currentUser.GetUid()
if err := storageService.UpdateStorageConfig(ctx, config); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
// Refresh providers after toggle to ensure correct provider state
if err := storageService.RefreshProviders(ctx); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{Data: map[string]bool{"enabled": config.Enabled}})
}
// GetStorageMetrics godoc
//
// @Tags storage
// @Summary Get storage usage metrics
// @Success 200 {object} HttpResponse{data=map[string]any}
// @Router /storage/metrics [get]
func (c *Controller) GetStorageMetrics(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
return
}
metrics, err := storageService.GetStorageMetrics(ctx)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, HttpResponse{Data: metrics})
}
// RefreshStorageMetrics godoc
//
// @Tags storage
// @Summary Refresh storage usage metrics
// @Success 200 {object} HttpResponse{}
// @Router /storage/metrics/refresh [post]
func (c *Controller) RefreshStorageMetrics(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &myErrors.ApiError{Code: myErrors.ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
if err := storageService.RefreshStorageMetrics(ctx); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &myErrors.ApiError{Code: myErrors.ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
}
// validateStorageConfig validates storage configuration
func validateStorageConfig(config *model.StorageConfig) error {
if config.Name == "" {
return &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "storage name is required"}}
}
if config.Type == "" {
return &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": "storage type is required"}}
}
return nil
}