diff --git a/README.md b/README.md index caea8a6..d3c641c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # ironmount mutagen sync create ~/Developer/ironmount nicolas@192.168.2.220:/home/nicolas/ironmount + +docker run --rm -it -v nicolas:/data alpine sh -lc 'echo hello > /data/hi && cat /data/hi' diff --git a/docker-compose.yml b/docker-compose.yml index 5f05cb4..fb266d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ services: - ./:/app/ - - /home/nicolas/ironmount/tmp:/mounts:rshared + - ./tmp:/mounts #:rshared environment: - GO_ENV=development - - VOLUME_ROOT=/home/nicolas/ironmount/tmp + - VOLUME_ROOT=/Users/nicolas/Developer/dir/ironmount/tmp diff --git a/go.mod b/go.mod index 9a9dd62..e6b0314 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,11 @@ go 1.24.5 require ( github.com/gin-gonic/gin v1.10.1 + github.com/glebarez/sqlite v1.11.0 github.com/go-playground/validator/v10 v10.27.0 github.com/rs/zerolog v1.34.0 github.com/spf13/viper v1.20.1 - modernc.org/sqlite v1.38.2 + gorm.io/gorm v1.30.1 ) require ( @@ -18,11 +19,14 @@ require ( github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -33,7 +37,6 @@ require ( github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rs/xid v1.6.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect @@ -55,4 +58,5 @@ require ( modernc.org/libc v1.66.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.38.2 // indirect ) diff --git a/go.sum b/go.sum index 3a737ce..ca73a3b 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,10 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -40,6 +44,10 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -73,7 +81,6 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= @@ -137,6 +144,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= +gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= diff --git a/internal/api/handlers.go b/internal/api/handlers.go deleted file mode 100644 index 4801f5c..0000000 --- a/internal/api/handlers.go +++ /dev/null @@ -1,19 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -// SetupHandlers sets up the API routes for the application. -func SetupHandlers(router *gin.Engine) { - router.GET("/api/health", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok"}) - }) - - router.GET("/api/volumes", ListVolumes) - router.POST("/api/volumes", CreateVolume) - router.GET("/api/volumes/:name", GetVolume) - router.DELETE("/api/volumes/:name", DeleteVolume) -} diff --git a/internal/api/volumes.go b/internal/api/volumes.go deleted file mode 100644 index 5613ea0..0000000 --- a/internal/api/volumes.go +++ /dev/null @@ -1,111 +0,0 @@ -package api - -import ( - "fmt" - "ironmount/internal/constants" - "ironmount/internal/core" - "ironmount/internal/db" - "net/http" - "os" - "path/filepath" - "strings" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" -) - -// CreateVolume handles the creation of a new volume. -func CreateVolume(c *gin.Context) { - var body CreateVolumeBody - if err := c.BindJSON(&body); err != nil { - log.Error().Err(err).Msg("Failed to bind JSON for CreateVolume request") - c.JSON(http.StatusBadRequest, gin.H{"err": "Invalid request body"}) - return - } - - cfg := core.LoadConfig() - - volPathHost := filepath.Join(cfg.VolumeRootHost, body.Name) - volPathLocal := filepath.Join(constants.VolumeRootLocal, body.Name) - - log.Info().Str("path", volPathLocal).Msg("Creating volume directory") - - if err := os.MkdirAll(volPathLocal, 0755); err != nil { - log.Error().Err(err).Str("path", volPathLocal).Msg("Failed to create volume directory") - - c.JSON(http.StatusInternalServerError, gin.H{"err": err.Error()}) - return - } - - if err := db.CreateVolume(body.Name, volPathHost); err != nil { - if strings.Contains(err.Error(), "UNIQUE") { - log.Warn().Err(err).Str("name", body.Name).Msg("Volume already exists") - c.JSON(http.StatusConflict, gin.H{"err": fmt.Sprintf("Volume %s already exists", body.Name)}) - return - } - - log.Error().Err(err).Str("name", body.Name).Msg("Failed to create volume in database") - c.JSON(http.StatusInternalServerError, gin.H{"Err": err.Error()}) - return - } - - // Create with docker volume driver - - c.JSON(200, CreateVolumeResponse{ - Name: body.Name, - Mountpoint: volPathHost, - Err: "", - }) -} - -func GetVolume(c *gin.Context) { - vol, err := db.GetVolumeByName(c.Param("name")) - if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - - c.JSON(200, GetVolumeResponse{ - Name: vol.Name, - Mountpoint: vol.Path, - Err: "", - }) -} - -func ListVolumes(c *gin.Context) { - vols, err := db.ListVolumes() - - if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - } - - volumes := []VolumeInfo{} - - for _, vol := range vols { - volumes = append(volumes, VolumeInfo{ - Name: vol.Name, - Mountpoint: vol.Path, - Err: "", - }) - } - - c.JSON(200, ListVolumesResponse{ - Volumes: volumes, - Err: "", - }) -} - -func DeleteVolume(c *gin.Context) { - if err := db.RemoveVolume(c.Param("name")); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - - vol, _ := db.GetVolumeByName(c.Param("name")) - if vol == nil { - c.JSON(404, gin.H{"error": "Volume not found"}) - return - } - - c.JSON(200, gin.H{"message": "Volume deleted successfully"}) -} diff --git a/internal/db/db.go b/internal/db/db.go index 6f7019c..a8aa9e3 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -1,96 +1,21 @@ package db import ( - "database/sql" - - "github.com/rs/zerolog/log" - _ "modernc.org/sqlite" + "github.com/glebarez/sqlite" + "gorm.io/gorm" ) -type Volume struct { - Name string `json:"name"` - Path string `json:"path"` - CreatedAt string `json:"created_at"` -} +var DB, err = gorm.Open(sqlite.Open("file:ironmount.db"), &gorm.Config{}) -// DB is the global database connection -var DB, err = sql.Open("sqlite", "file:ironmount.db") - -// Init initializes the database and creates the volumes table if it doesn't exist -func Init() { +// InitDB initializes the database and creates the volumes table if it doesn't exist +func InitDB() { if err != nil { panic(err) } - _, err = DB.Exec(` - CREATE TABLE IF NOT EXISTS volumes ( - name TEXT PRIMARY KEY, - path TEXT NOT NULL, - created_at TEXT DEFAULT (datetime('now')) - ); - `) + err = DB.AutoMigrate(&Volume{}) + if err != nil { panic(err) } } - -func GetVolumeByName(n string) (*Volume, error) { - var path string - var name string - - err := DB.QueryRow("SELECT name, path FROM volumes WHERE name = ?", n).Scan(&name, &path) - - if err != nil { - if err == sql.ErrNoRows { - return nil, nil - } - return nil, err - } - - return &Volume{ - Name: name, - Path: path, - }, nil -} - -func CreateVolume(name, path string) error { - _, err := DB.Exec("INSERT INTO volumes (name, path) VALUES (?, ?)", name, path) - if err != nil { - return err - } - return nil -} - -func RemoveVolume(name string) error { - _, err := DB.Exec("DELETE FROM volumes WHERE name = ?", name) - - log.Info().Str("volume", name).Msg("Removing volume") - if err != nil { - log.Error().Err(err).Str("volume", name).Msg("Error removing volume") - return err - } - return nil -} - -func ListVolumes() ([]Volume, error) { - rows, err := DB.Query("SELECT name, path FROM volumes") - if err != nil { - return nil, err - } - defer rows.Close() - - var volumes []Volume - for rows.Next() { - var vol Volume - if err := rows.Scan(&vol.Name, &vol.Path); err != nil { - return nil, err - } - volumes = append(volumes, vol) - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return volumes, nil -} diff --git a/internal/db/schema.go b/internal/db/schema.go new file mode 100644 index 0000000..22f52d3 --- /dev/null +++ b/internal/db/schema.go @@ -0,0 +1,12 @@ +package db + +import ( + + "gorm.io/gorm" +) + +type Volume struct { + gorm.Model + Name string `json:"name"` + Path string `json:"path"` +} diff --git a/internal/driver/activate.go b/internal/driver/activate.go deleted file mode 100644 index 213ba6e..0000000 --- a/internal/driver/activate.go +++ /dev/null @@ -1,15 +0,0 @@ -package driver - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -func Activate(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "Implements": []string{ - "VolumeDriver", - }, - }) -} diff --git a/internal/driver/capabilities.go b/internal/driver/capabilities.go deleted file mode 100644 index a5e7078..0000000 --- a/internal/driver/capabilities.go +++ /dev/null @@ -1,11 +0,0 @@ -package driver - -import "github.com/gin-gonic/gin" - -func Capabilities(c *gin.Context) { - c.JSON(200, gin.H{ - "Capabilities": map[string]bool{ - "Scope": true, // Indicates that the driver supports scope (local/global) - }, - }) -} diff --git a/internal/driver/create.go b/internal/driver/create.go deleted file mode 100644 index d3887e9..0000000 --- a/internal/driver/create.go +++ /dev/null @@ -1,15 +0,0 @@ -package driver - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" -) - -func Create(c *gin.Context) { - log.Error().Msg("Volumes can only be created through the API, not the driver interface") - c.JSON(http.StatusMethodNotAllowed, gin.H{ - "Err": "Volumes can only be created through the API, not the driver interface", - }) -} diff --git a/internal/driver/get.go b/internal/driver/get.go deleted file mode 100644 index fd6ebf6..0000000 --- a/internal/driver/get.go +++ /dev/null @@ -1,46 +0,0 @@ -package driver - -import ( - "fmt" - "ironmount/internal/db" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" -) - -func Get(c *gin.Context) { - var body GetRequest - - if err := c.BindJSON(&body); err != nil { - log.Error().Err(err).Msg("Failed to bind JSON for Get request") - c.JSON(http.StatusBadRequest, gin.H{"Err": err.Error()}) - return - } - - vol, err := db.GetVolumeByName(body.Name) - - fmt.Println("Get volume by name:", vol.Name) - - if err != nil { - log.Warn().Err(err).Str("name", body.Name).Msg("Failed to get volume by name") - response := map[string]string{ - "Err": err.Error(), - } - c.JSON(http.StatusNotFound, response) - - return - } - - response := map[string]any{ - "Volume": map[string]any{ - "Name": vol.Name, - "Mountpoint": vol.Path, - "Status": map[string]string{}, - // "CreatedAt": vol.CreatedAt, - }, - "Err": "", - } - - c.JSON(http.StatusOK, response) -} diff --git a/internal/driver/handlers.go b/internal/driver/handlers.go deleted file mode 100644 index 3e0f3ed..0000000 --- a/internal/driver/handlers.go +++ /dev/null @@ -1,17 +0,0 @@ -package driver - -import ( - "github.com/gin-gonic/gin" -) - -func SetupHandlers(router *gin.Engine) { - router.POST("/Plugin.Activate", Activate) - router.POST("/VolumeDriver.Create", Create) - router.POST("/VolumeDriver.Remove", Remove) - router.POST("/VolumeDriver.Mount", Mount) - router.POST("/VolumeDriver.Unmount", Unmount) - router.POST("/VolumeDriver.Path", Path) - router.POST("/VolumeDriver.Get", Get) - router.POST("/VolumeDriver.List", List) - router.POST("/VolumeDriver.Capabilities", Capabilities) -} diff --git a/internal/driver/list.go b/internal/driver/list.go deleted file mode 100644 index 4fd2ad7..0000000 --- a/internal/driver/list.go +++ /dev/null @@ -1,28 +0,0 @@ -package driver - -import ( - "ironmount/internal/db" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" -) - -func List(c *gin.Context) { - volumes, err := db.ListVolumes() - - if err != nil { - log.Error().Err(err).Msg("Failed to list volumes") - - c.JSON(http.StatusInternalServerError, gin.H{ - "Volumes": nil, - "Err": err.Error(), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "Volumes": volumes, - "Err": "", - }) -} diff --git a/internal/driver/mount.go b/internal/driver/mount.go deleted file mode 100644 index 280b9b2..0000000 --- a/internal/driver/mount.go +++ /dev/null @@ -1,36 +0,0 @@ -package driver - -import ( - "ironmount/internal/db" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" -) - -func Mount(c *gin.Context) { - var req MountRequest - if err := c.BindJSON(&req); err != nil { - log.Error().Err(err).Msg("Invalid request body") - - c.JSON(http.StatusBadRequest, gin.H{"Err": "Invalid request body"}) - return - } - - vol, err := db.GetVolumeByName(req.Name) - - if err != nil { - log.Error().Err(err).Str("volume", req.Name).Msg("Failed to get volume") - - c.JSON(http.StatusNotFound, gin.H{"Err": err.Error()}) - return - } - - log.Info().Str("volume", vol.Name).Str("path", vol.Path).Msg("Mounting volume") - - c.JSON(http.StatusOK, gin.H{ - "Name": vol.Name, - "Mountpoint": vol.Path, - "Err": "", - }) -} diff --git a/internal/driver/path.go b/internal/driver/path.go deleted file mode 100644 index 63df9b1..0000000 --- a/internal/driver/path.go +++ /dev/null @@ -1,32 +0,0 @@ -package driver - -import ( - "ironmount/internal/db" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" -) - -func Path(c *gin.Context) { - var req PathRequest - - if err := c.BindJSON(&req); err != nil { - log.Error().Err(err).Msg("Invalid request body for Path") - c.JSON(http.StatusBadRequest, gin.H{"Err": "Invalid request body"}) - return - } - - vol, err := db.GetVolumeByName(req.Name) - if err != nil { - log.Error().Err(err).Str("volume", req.Name).Msg("Failed to get volume by name") - - c.JSON(http.StatusNotFound, gin.H{"Err": err.Error()}) - return - } - - c.JSON(http.StatusOK, gin.H{ - "Mountpoint": vol.Path, - "Err": "", - }) -} diff --git a/internal/driver/remove.go b/internal/driver/remove.go deleted file mode 100644 index 9c839ba..0000000 --- a/internal/driver/remove.go +++ /dev/null @@ -1,40 +0,0 @@ -package driver - -import ( - "ironmount/internal/constants" - "ironmount/internal/db" - "net/http" - "os" - "path/filepath" - - "github.com/gin-gonic/gin" - "github.com/rs/zerolog/log" -) - -func Remove(c *gin.Context) { - var req RemoveRequest - - if err := c.BindJSON(&req); err != nil { - log.Error().Err(err).Msg("Invalid request body for Remove") - c.JSON(http.StatusBadRequest, gin.H{"Err": "Invalid request body"}) - return - } - - vol, err := db.GetVolumeByName(req.Name) - if err != nil { - log.Error().Err(err).Str("volume", req.Name).Msg("Failed to get volume by name") - - c.JSON(http.StatusNotFound, gin.H{"Err": err.Error()}) - return - } - - db.RemoveVolume(vol.Name) - - volPathLocal := filepath.Join(constants.VolumeRootLocal, req.Name) - log.Info().Str("path", volPathLocal).Msg("Removing volume directory") - os.RemoveAll(volPathLocal) - - c.JSON(http.StatusOK, gin.H{ - "Err": "", - }) -} diff --git a/internal/driver/unmount.go b/internal/driver/unmount.go deleted file mode 100644 index bc195d2..0000000 --- a/internal/driver/unmount.go +++ /dev/null @@ -1,13 +0,0 @@ -package driver - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -func Unmount(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "Err": "", - }) -} diff --git a/internal/modules/driver/handlers.go b/internal/modules/driver/handlers.go new file mode 100644 index 0000000..cfbfde6 --- /dev/null +++ b/internal/modules/driver/handlers.go @@ -0,0 +1,177 @@ +// Package driver provides the HTTP handlers for the volume driver API. +package driver + +import ( + "ironmount/internal/modules/volumes" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +func SetupHandlers(router *gin.Engine) { + + volumeService := volumes.VolumeService{} + + router.POST("/VolumeDriver.Capabilities", func(c *gin.Context) { + c.JSON(200, gin.H{ + "Capabilities": map[string]bool{ + "Scope": true, // Indicates that the driver supports scope (local/global) + }, + }) + }) + + router.POST("/Plugin.Activate", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "Implements": []string{ + "VolumeDriver", + }, + }) + }) + + router.POST("/VolumeDriver.Create", func(c *gin.Context) { + var req CreateRequest + if err := c.BindJSON(&req); err != nil { + log.Error().Err(err).Msg("Invalid request body for Create") + c.JSON(http.StatusBadRequest, gin.H{"Err": "Invalid request body"}) + return + } + + volume, status, err := volumeService.CreateVolume(req.Name) + + if err != nil { + log.Error().Err(err).Msg("Failed to create volume") + c.JSON(status, gin.H{"Err": err.Error()}) + return + } + + c.JSON(status, gin.H{ + "Name": volume.Name, + "Mountpoint": volume.Path, + "Err": "", + }) + }) + + router.POST("/VolumeDriver.Remove", func(c *gin.Context) { + var req RemoveRequest + + if err := c.BindJSON(&req); err != nil { + log.Error().Err(err).Msg("Invalid request body for Remove") + c.JSON(http.StatusBadRequest, gin.H{"Err": "Invalid request body"}) + return + } + + status, err := volumeService.DeleteVolume(req.Name) + + if err != nil { + c.JSON(status, gin.H{"Err": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "Err": "", + }) + }) + + router.POST("/VolumeDriver.Mount", func(c *gin.Context) { + var req MountRequest + if err := c.BindJSON(&req); err != nil { + log.Error().Err(err).Msg("Invalid request body") + + c.JSON(http.StatusBadRequest, gin.H{"Err": "Invalid request body"}) + return + } + + volume, err := volumeService.GetVolume(req.Name) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if volume == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Volume not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "Name": volume.Name, + "Mountpoint": volume.Path, + "Err": "", + }) + }) + + // VolumeDriver.Unmount is a no-op in this implementation + router.POST("/VolumeDriver.Unmount", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "Err": "", + }) + }) + + // VolumeDriver.Path returns the mount point of the volume + router.POST("/VolumeDriver.Path", func(c *gin.Context) { + var req PathRequest + + if err := c.BindJSON(&req); err != nil { + log.Error().Err(err).Msg("Invalid request body for Path") + c.JSON(http.StatusBadRequest, gin.H{"Err": "Invalid request body"}) + return + } + + vol, err := volumeService.GetVolume(req.Name) + + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"Err": err.Error()}) + return + } + + if vol == nil { + c.JSON(http.StatusNotFound, gin.H{"Err": "Volume not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "Mountpoint": vol.Path, + "Err": "", + }) + }) + + router.POST("/VolumeDriver.Get", func(c *gin.Context) { + var req GetRequest + if err := c.BindJSON(&req); err != nil { + log.Error().Err(err).Msg("Invalid request body for Get") + c.JSON(http.StatusBadRequest, gin.H{"Err": "Invalid request body"}) + return + } + + vol, err := volumeService.GetVolume(req.Name) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"Err": err.Error()}) + return + } + + if vol == nil { + c.JSON(http.StatusNotFound, gin.H{"Err": "Volume not found"}) + return + } + + data := map[string]any{ + "Volume": map[string]string{ + "Name": vol.Name, + "Mountpoint": vol.Path, + "CreatedAt": vol.CreatedAt.Format(volumes.DateFormat), + }, + "Err": "", + } + + c.JSON(http.StatusOK, data) + }) + + router.POST("/VolumeDriver.List", func(c *gin.Context) { + volumesList := volumeService.ListVolumes() + + c.JSON(http.StatusOK, gin.H{ + "Volumes": volumesList, + }) + }) +} diff --git a/internal/driver/type.go b/internal/modules/driver/types.go similarity index 100% rename from internal/driver/type.go rename to internal/modules/driver/types.go diff --git a/internal/modules/volumes/handlers.go b/internal/modules/volumes/handlers.go new file mode 100644 index 0000000..cc604e2 --- /dev/null +++ b/internal/modules/volumes/handlers.go @@ -0,0 +1,73 @@ +// Package volumes provides tools for managing volumes in the application. +package volumes + +import ( + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +// SetupHandlers sets up the API routes for the application. +func SetupHandlers(router *gin.Engine) { + + volumeService := VolumeService{} + + router.GET("/api/volumes", func(c *gin.Context) { + volumes := volumeService.ListVolumes() + + log.Debug().Msgf("Listing volumes: %v", volumes) + + c.JSON(200, gin.H{ + "volumes": volumes, + }) + }) + + router.POST("/api/volumes", func(c *gin.Context) { + var req struct { + Name string `json:"name" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"error": "Invalid request body"}) + return + } + + volume, status, err := volumeService.CreateVolume(req.Name) + + if err != nil { + c.JSON(status, gin.H{"error": err.Error()}) + return + } + c.JSON(status, volume) + }) + + router.GET("/api/volumes/:name", func(c *gin.Context) { + volume, err := volumeService.GetVolume(c.Param("name")) + + if err != nil { + c.JSON(500, gin.H{"error": err.Error()}) + return + } + + if volume == nil { + c.JSON(404, gin.H{"error": "Volume not found"}) + return + } + + c.JSON(200, gin.H{ + "name": volume.Name, + "mountpoint": volume.Path, + "created_at": volume.CreatedAt.String(), + "err": "", + }) + }) + + router.DELETE("/api/volumes/:name", func(c *gin.Context) { + status, err := volumeService.DeleteVolume(c.Param("name")) + + if err != nil { + c.JSON(status, gin.H{"error": err.Error()}) + return + } + + c.JSON(200, gin.H{"message": "Volume deleted successfully"}) + }) +} diff --git a/internal/modules/volumes/queries.go b/internal/modules/volumes/queries.go new file mode 100644 index 0000000..8b0e37d --- /dev/null +++ b/internal/modules/volumes/queries.go @@ -0,0 +1,62 @@ +package volumes + +import ( + "context" + "ironmount/internal/db" + + "github.com/rs/zerolog/log" + "gorm.io/gorm" +) + +type VolumeQueries struct{} + +func (q *VolumeQueries) QueryVolumeByName(n string) (*db.Volume, error) { + ctx := context.Background() + + volume, err := gorm.G[*db.Volume](db.DB).Where("name = ?", n).First(ctx) + + if err != nil { + if (err.Error() == "record not found") || (err == gorm.ErrRecordNotFound) { + log.Warn().Str("name", n).Msg("Volume not found") + return nil, nil + } + + return nil, err + } + + return volume, nil +} + +func (q *VolumeQueries) InsertVolume(name, path string) error { + ctx := context.Background() + err := gorm.G[db.Volume](db.DB).Create(ctx, &db.Volume{Name: name, Path: path}) + + if err != nil { + return err + } + + return nil +} + +func (q *VolumeQueries) RemoveVolume(name string) error { + ctx := context.Background() + + log.Info().Str("volume", name).Msg("Removing volume") + _, err := gorm.G[db.Volume](db.DB).Where("name = ?", name).Delete(ctx) + + if err != nil { + log.Error().Err(err).Str("volume", name).Msg("Error removing volume") + return err + } + return nil +} + +func (q *VolumeQueries) QueryVolumes() ([]db.Volume, error) { + rows, err := gorm.G[db.Volume](db.DB).Select("name", "path", "created_at").Find(context.Background()) + + if err != nil { + return []db.Volume{}, err + } + + return rows, nil +} diff --git a/internal/modules/volumes/service.go b/internal/modules/volumes/service.go new file mode 100644 index 0000000..3e2ef6a --- /dev/null +++ b/internal/modules/volumes/service.go @@ -0,0 +1,85 @@ +package volumes + +import ( + "fmt" + "ironmount/internal/constants" + "ironmount/internal/core" + "ironmount/internal/db" + "net/http" + "os" + "path/filepath" + "strings" +) + +type VolumeService struct{} + +var volumeQueries = VolumeQueries{} + +// CreateVolume handles the creation of a new volume. +func (v *VolumeService) CreateVolume(name string) (*db.Volume, int, error) { + existingVol, _ := volumeQueries.QueryVolumeByName(name) + + if existingVol != nil { + return nil, http.StatusConflict, fmt.Errorf("volume %s already exists", name) + } + + cfg := core.LoadConfig() + + volPathHost := filepath.Join(cfg.VolumeRootHost, name) + volPathLocal := filepath.Join(constants.VolumeRootLocal, name) + + if err := os.MkdirAll(volPathLocal, 0755); err != nil { + return nil, http.StatusInternalServerError, fmt.Errorf("failed to create volume directory: %w", err) + } + + if err := volumeQueries.InsertVolume(name, volPathHost); err != nil { + if strings.Contains(err.Error(), "UNIQUE") { + return nil, http.StatusConflict, fmt.Errorf("volume %s already exists", name) + } + + return nil, http.StatusInternalServerError, fmt.Errorf("failed to create volume in database: %w", err) + } + + return &db.Volume{ + Name: name, + Path: volPathHost, + }, http.StatusOK, nil +} + +func (v *VolumeService) GetVolume(name string) (*db.Volume, error) { + vol, err := volumeQueries.QueryVolumeByName(name) + + return vol, err +} + +func (v *VolumeService) ListVolumes() []VolumeInfo { + vols, _ := volumeQueries.QueryVolumes() + volumes := []VolumeInfo{} + + for _, vol := range vols { + volumes = append(volumes, VolumeInfo{ + Name: vol.Name, + Mountpoint: vol.Path, + CreatedAt: vol.CreatedAt.Format(DateFormat), + Err: "", + }) + } + + return volumes +} + +func (v *VolumeService) DeleteVolume(name string) (int, error) { + vol, _ := volumeQueries.QueryVolumeByName(name) + + if vol == nil { + return http.StatusNotFound, fmt.Errorf("volume %s not found", name) + } + + if err := volumeQueries.RemoveVolume(name); err != nil { + return http.StatusInternalServerError, fmt.Errorf("failed to remove volume from database: %w", err) + } + + // os.RemoveAll(vol.Path) ?? depends on whether we want to delete the actual directory + + return http.StatusOK, nil +} diff --git a/internal/api/types.go b/internal/modules/volumes/types.go similarity index 85% rename from internal/api/types.go rename to internal/modules/volumes/types.go index b4161d6..58f07b7 100644 --- a/internal/api/types.go +++ b/internal/modules/volumes/types.go @@ -1,4 +1,6 @@ -package api +package volumes + +var DateFormat = "2006-01-02T15:04:05Z" type CreateVolumeBody struct { Name string `json:"name" binding:"required"` @@ -20,6 +22,7 @@ type VolumeInfo struct { Name string `json:"name"` Mountpoint string `json:"mountpoint"` Err string `json:"err,omitempty"` + CreatedAt string `json:"created_at,omitempty"` } type ListVolumesResponse struct { diff --git a/main.go b/main.go index 45c5d32..9da5594 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,11 @@ package main import ( - "ironmount/internal/api" "ironmount/internal/constants" "ironmount/internal/core" "ironmount/internal/db" - "ironmount/internal/driver" + "ironmount/internal/modules/driver" + "ironmount/internal/modules/volumes" "net" "net/http" @@ -15,13 +15,8 @@ import ( "github.com/rs/zerolog/log" ) -type Volume struct { - Name string - Path string -} - func main() { - db.Init() + db.InitDB() if err := os.MkdirAll("/run/docker/plugins", 0755); err != nil { log.Fatal().Err(err).Msg("Failed to create plugin directory") @@ -46,8 +41,12 @@ func main() { router.Use(core.GinLogger()) router.Use(gin.Recovery()) + router.GET("/api/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + }) + driver.SetupHandlers(router) - api.SetupHandlers(router) + volumes.SetupHandlers(router) unixListener, err := net.Listen("unix", socketPath) if err != nil {