diff --git a/docker-compose.yml b/docker-compose.yml index 2d61235..5f05cb4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: ironmount: build: @@ -20,7 +18,7 @@ services: - ./:/app/ - - /home/nicolas/ironmount/tmp:/mounts + - /home/nicolas/ironmount/tmp:/mounts:rshared environment: - GO_ENV=development - VOLUME_ROOT=/home/nicolas/ironmount/tmp diff --git a/internal/api/handlers.go b/internal/api/handlers.go new file mode 100644 index 0000000..4801f5c --- /dev/null +++ b/internal/api/handlers.go @@ -0,0 +1,19 @@ +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/types.go b/internal/api/types.go new file mode 100644 index 0000000..b4161d6 --- /dev/null +++ b/internal/api/types.go @@ -0,0 +1,28 @@ +package api + +type CreateVolumeBody struct { + Name string `json:"name" binding:"required"` +} + +type CreateVolumeResponse struct { + Name string `json:"name"` + Mountpoint string `json:"mountpoint"` + Err string `json:"err,omitempty"` +} + +type GetVolumeResponse struct { + Name string `json:"name"` + Mountpoint string `json:"mountpoint"` + Err string `json:"err,omitempty"` +} + +type VolumeInfo struct { + Name string `json:"name"` + Mountpoint string `json:"mountpoint"` + Err string `json:"err,omitempty"` +} + +type ListVolumesResponse struct { + Volumes []VolumeInfo `json:"volumes"` + Err string `json:"err,omitempty"` +} diff --git a/internal/api/volumes.go b/internal/api/volumes.go new file mode 100644 index 0000000..5613ea0 --- /dev/null +++ b/internal/api/volumes.go @@ -0,0 +1,111 @@ +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 1810eda..6f7019c 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -8,8 +8,9 @@ import ( ) type Volume struct { - Name string `json:"name"` - Path string `json:"path"` + Name string `json:"name"` + Path string `json:"path"` + CreatedAt string `json:"created_at"` } // DB is the global database connection @@ -24,7 +25,8 @@ func Init() { _, err = DB.Exec(` CREATE TABLE IF NOT EXISTS volumes ( name TEXT PRIMARY KEY, - path TEXT NOT NULL + path TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')) ); `) if err != nil { @@ -39,6 +41,9 @@ func GetVolumeByName(n string) (*Volume, error) { 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 } diff --git a/internal/driver/create.go b/internal/driver/create.go index 8954b17..d3887e9 100644 --- a/internal/driver/create.go +++ b/internal/driver/create.go @@ -1,46 +1,15 @@ package driver import ( - "ironmount/internal/constants" - "ironmount/internal/core" - "ironmount/internal/db" "net/http" - "os" - "path/filepath" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" ) func Create(c *gin.Context) { - var body CreateRequest - - if err := c.BindJSON(&body); err != nil { - log.Error().Err(err).Msg("Failed to bind JSON for Create request") - c.JSON(http.StatusBadRequest, gin.H{"Err": err.Error()}) - 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 - } - - db.CreateVolume(body.Name, volPathHost) - - response := map[string]string{ - "Name": body.Name, - "Mountpoint": volPathHost, - "Err": "", - } - - c.JSON(http.StatusOK, response) + 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 index 24fa6eb..fd6ebf6 100644 --- a/internal/driver/get.go +++ b/internal/driver/get.go @@ -1,6 +1,7 @@ package driver import ( + "fmt" "ironmount/internal/db" "net/http" @@ -19,6 +20,8 @@ func Get(c *gin.Context) { 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{ @@ -34,6 +37,7 @@ func Get(c *gin.Context) { "Name": vol.Name, "Mountpoint": vol.Path, "Status": map[string]string{}, + // "CreatedAt": vol.CreatedAt, }, "Err": "", } diff --git a/main.go b/main.go index 95c56e7..45c5d32 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "ironmount/internal/api" "ironmount/internal/constants" "ironmount/internal/core" "ironmount/internal/db" @@ -19,8 +20,6 @@ type Volume struct { Path string } -var volumes = map[string]Volume{} - func main() { db.Init() @@ -48,6 +47,7 @@ func main() { router.Use(gin.Recovery()) driver.SetupHandlers(router) + api.SetupHandlers(router) unixListener, err := net.Listen("unix", socketPath) if err != nil {