package store import ( "database/sql" "errors" "fmt" "strings" "github.com/google/uuid" ) // SetWorkloadVolume upserts a volume mount keyed by (workload_id, target). // The target is the natural key — re-using a target replaces the row // rather than accumulating duplicates that would conflict at mount time. func (s *Store) SetWorkloadVolume(v WorkloadVolume) (WorkloadVolume, error) { if v.WorkloadID == "" || v.Target == "" { return WorkloadVolume{}, fmt.Errorf("workload_volume: workload_id and target are required") } if v.Scope == "" { v.Scope = string(VolumeScopeAbsolute) } if !IsValidVolumeScope(v.Scope) { return WorkloadVolume{}, fmt.Errorf("workload_volume: invalid scope %q", v.Scope) } if v.ID == "" { v.ID = uuid.New().String() } now := Now() if _, err := s.db.Exec( `INSERT INTO workload_volumes (id, workload_id, source, target, scope, name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(workload_id, target) DO UPDATE SET source = excluded.source, scope = excluded.scope, name = excluded.name, updated_at = excluded.updated_at`, v.ID, v.WorkloadID, v.Source, v.Target, v.Scope, v.Name, now, now, ); err != nil { return WorkloadVolume{}, fmt.Errorf("upsert workload volume: %w", err) } return s.getWorkloadVolumeByTarget(v.WorkloadID, v.Target) } // ListWorkloadVolumes returns every mount for the given workload, ordered // by target so the UI rendering is stable across requests. func (s *Store) ListWorkloadVolumes(workloadID string) ([]WorkloadVolume, error) { rows, err := s.db.Query( `SELECT id, workload_id, source, target, scope, name, created_at, updated_at FROM workload_volumes WHERE workload_id = ? ORDER BY target`, workloadID, ) if err != nil { return nil, fmt.Errorf("query workload volumes: %w", err) } defer rows.Close() out := []WorkloadVolume{} for rows.Next() { v, err := scanWorkloadVolume(rows) if err != nil { return nil, err } out = append(out, v) } return out, rows.Err() } // DeleteWorkloadVolume removes one mount by ID. func (s *Store) DeleteWorkloadVolume(id string) error { result, err := s.db.Exec(`DELETE FROM workload_volumes WHERE id = ?`, id) if err != nil { return fmt.Errorf("delete workload volume: %w", err) } n, _ := result.RowsAffected() if n == 0 { return fmt.Errorf("workload volume %s: %w", id, ErrNotFound) } return nil } func (s *Store) getWorkloadVolumeByTarget(workloadID, target string) (WorkloadVolume, error) { var v WorkloadVolume err := s.db.QueryRow( `SELECT id, workload_id, source, target, scope, name, created_at, updated_at FROM workload_volumes WHERE workload_id = ? AND target = ?`, workloadID, target, ).Scan(&v.ID, &v.WorkloadID, &v.Source, &v.Target, &v.Scope, &v.Name, &v.CreatedAt, &v.UpdatedAt) if errors.Is(err, sql.ErrNoRows) { return WorkloadVolume{}, fmt.Errorf("workload volume (%s,%s): %w", workloadID, target, ErrNotFound) } if err != nil { return WorkloadVolume{}, fmt.Errorf("query workload volume: %w", err) } return v, nil } func scanWorkloadVolume(rows *sql.Rows) (WorkloadVolume, error) { var v WorkloadVolume if err := rows.Scan(&v.ID, &v.WorkloadID, &v.Source, &v.Target, &v.Scope, &v.Name, &v.CreatedAt, &v.UpdatedAt); err != nil { return WorkloadVolume{}, fmt.Errorf("scan workload volume: %w", err) } return v, nil } // normalizeAbsolutePath is a defensive helper for volume source paths in // "absolute" scope. Rejects path-traversal segments so a malicious client // can't escape an allow-listed prefix at the API layer. The actual // allowed-paths check lives in settings.AllowedVolumePaths and remains // the policy authority. func normalizeAbsolutePath(p string) string { p = strings.TrimSpace(p) if p == "" { return "" } if strings.Contains(p, "..") { return "" } return p }