logging and diagnostic features added
Some checks failed
CI / test-build (push) Failing after 2m11s

This commit is contained in:
2025-12-15 00:45:14 +07:00
parent 3e64de18ed
commit df475bc85e
26 changed files with 5878 additions and 91 deletions

View File

@@ -9,8 +9,10 @@ import (
"strings"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/auth"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/errors"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/models"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/storage"
"gitea.avt.data-center.id/othman.suseno/atlas/internal/validation"
)
// pathParam is now in router_helpers.go
@@ -45,12 +47,18 @@ func (a *App) handleCreatePool(w http.ResponseWriter, r *http.Request) {
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
writeError(w, errors.ErrBadRequest("invalid request body"))
return
}
if req.Name == "" || len(req.VDEVs) == 0 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and vdevs are required"})
// Validate pool name
if err := validation.ValidateZFSName(req.Name); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
if len(req.VDEVs) == 0 {
writeError(w, errors.ErrValidation("at least one vdev is required"))
return
}
@@ -224,17 +232,31 @@ func (a *App) handleListZVOLs(w http.ResponseWriter, r *http.Request) {
func (a *App) handleCreateZVOL(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Size uint64 `json:"size"` // in bytes
Size string `json:"size"` // human-readable format (e.g., "10G")
Options map[string]string `json:"options,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
writeError(w, errors.ErrBadRequest("invalid request body"))
return
}
if req.Name == "" || req.Size == 0 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and size are required"})
// Validate ZVOL name
if err := validation.ValidateZFSName(req.Name); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
// Validate size format
if err := validation.ValidateSize(req.Size); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
// Parse size to bytes
sizeBytes, err := a.parseSizeString(req.Size)
if err != nil {
writeError(w, errors.ErrValidation(fmt.Sprintf("invalid size: %v", err)))
return
}
@@ -242,7 +264,7 @@ func (a *App) handleCreateZVOL(w http.ResponseWriter, r *http.Request) {
req.Options = make(map[string]string)
}
if err := a.zfs.CreateZVOL(req.Name, req.Size, req.Options); err != nil {
if err := a.zfs.CreateZVOL(req.Name, sizeBytes, req.Options); err != nil {
log.Printf("create zvol error: %v", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
@@ -314,8 +336,16 @@ func (a *App) handleCreateSnapshot(w http.ResponseWriter, r *http.Request) {
return
}
if req.Dataset == "" || req.Name == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "dataset and name are required"})
// Validate dataset name
if err := validation.ValidateZFSName(req.Dataset); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
// Validate snapshot name (can contain @ but we'll validate the base name)
snapshotBaseName := strings.ReplaceAll(req.Name, "@", "")
if err := validation.ValidateZFSName(snapshotBaseName); err != nil {
writeError(w, errors.ErrValidation("invalid snapshot name"))
return
}
@@ -325,10 +355,10 @@ func (a *App) handleCreateSnapshot(w http.ResponseWriter, r *http.Request) {
return
}
snapshotName := fmt.Sprintf("%s@%s", req.Dataset, req.Name)
snap, err := a.zfs.GetSnapshot(snapshotName)
fullSnapshotName := fmt.Sprintf("%s@%s", req.Dataset, req.Name)
snap, err := a.zfs.GetSnapshot(fullSnapshotName)
if err != nil {
writeJSON(w, http.StatusCreated, map[string]string{"message": "snapshot created", "name": snapshotName})
writeJSON(w, http.StatusCreated, map[string]string{"message": "snapshot created", "name": fullSnapshotName})
return
}
@@ -477,11 +507,27 @@ func (a *App) handleCreateSMBShare(w http.ResponseWriter, r *http.Request) {
return
}
if req.Name == "" || req.Dataset == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and dataset are required"})
// Validate share name
if err := validation.ValidateShareName(req.Name); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
// Validate dataset name
if err := validation.ValidateZFSName(req.Dataset); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
// Sanitize path if provided
if req.Path != "" {
req.Path = validation.SanitizePath(req.Path)
if err := validation.ValidatePath(req.Path); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
}
// Validate dataset exists
datasets, err := a.zfs.ListDatasets("")
if err != nil {
@@ -509,20 +555,22 @@ func (a *App) handleCreateSMBShare(w http.ResponseWriter, r *http.Request) {
share, err := a.smbStore.Create(req.Name, req.Path, req.Dataset, req.Description, req.ReadOnly, req.GuestOK, req.ValidUsers)
if err != nil {
if err == storage.ErrSMBShareExists {
writeJSON(w, http.StatusConflict, map[string]string{"error": "share name already exists"})
writeError(w, errors.ErrConflict("share name already exists"))
return
}
log.Printf("create SMB share error: %v", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
writeError(w, errors.ErrInternal("failed to create SMB share").WithDetails(err.Error()))
return
}
// Apply configuration to Samba service
// Apply configuration to Samba service (with graceful degradation)
shares := a.smbStore.List()
if err := a.smbService.ApplyConfiguration(shares); err != nil {
log.Printf("apply SMB configuration error: %v", err)
// Don't fail the request, but log the error
// In production, you might want to queue this for retry
// Log but don't fail the request - desired state is stored
// Service configuration can be retried later
if svcErr := a.handleServiceError("SMB", err); svcErr != nil {
log.Printf("SMB service configuration failed (non-fatal): %v", err)
}
}
writeJSON(w, http.StatusCreated, share)
@@ -629,11 +677,29 @@ func (a *App) handleCreateNFSExport(w http.ResponseWriter, r *http.Request) {
return
}
if req.Dataset == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "dataset is required"})
// Validate dataset name
if err := validation.ValidateZFSName(req.Dataset); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
// Validate and sanitize path if provided
if req.Path != "" {
req.Path = validation.SanitizePath(req.Path)
if err := validation.ValidatePath(req.Path); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
}
// Validate clients
for i, client := range req.Clients {
if err := validation.ValidateCIDR(client); err != nil {
writeError(w, errors.ErrValidation(fmt.Sprintf("client[%d]: %s", i, err.Error())))
return
}
}
// Validate dataset exists
datasets, err := a.zfs.ListDatasets("")
if err != nil {
@@ -786,14 +852,9 @@ func (a *App) handleCreateISCSITarget(w http.ResponseWriter, r *http.Request) {
return
}
if req.IQN == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "iqn is required"})
return
}
// Basic IQN format validation (iqn.yyyy-mm.reversed.domain:identifier)
if !strings.HasPrefix(req.IQN, "iqn.") {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid IQN format (must start with 'iqn.')"})
// Validate IQN format
if err := validation.ValidateIQN(req.IQN); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
@@ -1065,8 +1126,14 @@ func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) {
return
}
if req.Username == "" || req.Password == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password are required"})
// Validate username (login is less strict - just check not empty)
if req.Username == "" {
writeError(w, errors.ErrValidation("username is required"))
return
}
if req.Password == "" {
writeError(w, errors.ErrValidation("password is required"))
return
}
@@ -1116,11 +1183,26 @@ func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) {
return
}
if req.Username == "" || req.Password == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password are required"})
// Validate username
if err := validation.ValidateUsername(req.Username); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
// Validate password
if err := validation.ValidatePassword(req.Password); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
// Validate email if provided
if req.Email != "" {
if err := validation.ValidateEmail(req.Email); err != nil {
writeError(w, errors.ErrValidation(err.Error()))
return
}
}
if req.Role == "" {
req.Role = models.RoleViewer // Default role
}