From f67411de2c6aec62a8ef632bd92f60f996ade916 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:27:21 +0100 Subject: [PATCH 1/3] feat(storage): add CRUD operations for persistent and file storages Add comprehensive storage management system for applications, databases, and services: - Implement storage subcommands (list, create, update, delete) with full API integration - Add support for both persistent volumes and file-based storage management - Create Storage model with comprehensive validation for type-specific operations - Implement ApplicationService, DatabaseService, and ServiceService storage methods - Add extensive unit tests covering CRUD operations and edge cases - Integrate storage subcommand into application, database, and service CLI commands - Add dockerfile-target-build flag support to application creation and update commands Storage operations support: - Persistent volumes: create with optional host paths, update mount paths and names - File storages: create/update with content or file system paths, support directories - Common features: mount path management, read-only detection, preview suffix toggling --- cmd/application/application.go | 14 ++ cmd/application/create/deploy_key.go | 2 + cmd/application/create/dockerfile.go | 2 + cmd/application/create/dockerimage.go | 2 + cmd/application/create/github.go | 2 + cmd/application/create/public.go | 2 + cmd/application/storage/create.go | 92 +++++++++ cmd/application/storage/delete.go | 39 ++++ cmd/application/storage/list.go | 47 +++++ cmd/application/storage/update.go | 113 +++++++++++ cmd/application/update.go | 6 + cmd/database/database.go | 14 ++ cmd/database/storage/create.go | 90 +++++++++ cmd/database/storage/delete.go | 39 ++++ cmd/database/storage/list.go | 47 +++++ cmd/database/storage/update.go | 110 +++++++++++ cmd/service/service.go | 14 ++ cmd/service/storage/create.go | 99 ++++++++++ cmd/service/storage/delete.go | 39 ++++ cmd/service/storage/list.go | 47 +++++ cmd/service/storage/update.go | 110 +++++++++++ internal/models/application.go | 198 +++++++++++++++---- internal/service/application.go | 37 ++++ internal/service/application_test.go | 261 ++++++++++++++++++++++++++ internal/service/database.go | 37 ++++ internal/service/database_test.go | 121 ++++++++++++ internal/service/service.go | 37 ++++ internal/service/service_test.go | 111 +++++++++++ 28 files changed, 1698 insertions(+), 34 deletions(-) create mode 100644 cmd/application/storage/create.go create mode 100644 cmd/application/storage/delete.go create mode 100644 cmd/application/storage/list.go create mode 100644 cmd/application/storage/update.go create mode 100644 cmd/database/storage/create.go create mode 100644 cmd/database/storage/delete.go create mode 100644 cmd/database/storage/list.go create mode 100644 cmd/database/storage/update.go create mode 100644 cmd/service/storage/create.go create mode 100644 cmd/service/storage/delete.go create mode 100644 cmd/service/storage/list.go create mode 100644 cmd/service/storage/update.go diff --git a/cmd/application/application.go b/cmd/application/application.go index 514cfbe..4a4c76f 100644 --- a/cmd/application/application.go +++ b/cmd/application/application.go @@ -5,6 +5,7 @@ import ( "github.com/coollabsio/coolify-cli/cmd/application/create" "github.com/coollabsio/coolify-cli/cmd/application/env" + "github.com/coollabsio/coolify-cli/cmd/application/storage" ) // NewAppCommand creates the app parent command @@ -43,5 +44,18 @@ func NewAppCommand() *cobra.Command { envCmd.AddCommand(env.NewSyncEnvCommand()) cmd.AddCommand(envCmd) + // Add storage subcommand with its children + storageCmd := &cobra.Command{ + Use: "storage", + Aliases: []string{"storages"}, + Short: "Manage application storages", + Long: `List and manage persistent volumes and file storages for applications.`, + } + storageCmd.AddCommand(storage.NewListCommand()) + storageCmd.AddCommand(storage.NewCreateCommand()) + storageCmd.AddCommand(storage.NewUpdateCommand()) + storageCmd.AddCommand(storage.NewDeleteCommand()) + cmd.AddCommand(storageCmd) + return cmd } diff --git a/cmd/application/create/deploy_key.go b/cmd/application/create/deploy_key.go index 37c2f0c..f65b086 100644 --- a/cmd/application/create/deploy_key.go +++ b/cmd/application/create/deploy_key.go @@ -93,6 +93,7 @@ Examples: setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy) setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled) setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath) + setOptionalStringFlag(cmd, "dockerfile-target-build", &req.DockerfileTargetBuild) client, err := cli.GetAPIClient(cmd) if err != nil { @@ -147,6 +148,7 @@ Examples: cmd.Flags().String("limits-memory", "", "Memory limit") cmd.Flags().Bool("health-check-enabled", false, "Enable health checks") cmd.Flags().String("health-check-path", "", "Health check path") + cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage") return cmd } diff --git a/cmd/application/create/dockerfile.go b/cmd/application/create/dockerfile.go index 5f0f0f1..4db9d62 100644 --- a/cmd/application/create/dockerfile.go +++ b/cmd/application/create/dockerfile.go @@ -70,6 +70,7 @@ Examples: setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy) setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled) setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath) + setOptionalStringFlag(cmd, "dockerfile-target-build", &req.DockerfileTargetBuild) client, err := cli.GetAPIClient(cmd) if err != nil { @@ -115,6 +116,7 @@ Examples: cmd.Flags().String("limits-memory", "", "Memory limit") cmd.Flags().Bool("health-check-enabled", false, "Enable health checks") cmd.Flags().String("health-check-path", "", "Health check path") + cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage") return cmd } diff --git a/cmd/application/create/dockerimage.go b/cmd/application/create/dockerimage.go index 8920ccd..9cb1cab 100644 --- a/cmd/application/create/dockerimage.go +++ b/cmd/application/create/dockerimage.go @@ -76,6 +76,7 @@ Examples: setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy) setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled) setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath) + setOptionalStringFlag(cmd, "dockerfile-target-build", &req.DockerfileTargetBuild) client, err := cli.GetAPIClient(cmd) if err != nil { @@ -122,6 +123,7 @@ Examples: cmd.Flags().String("limits-memory", "", "Memory limit") cmd.Flags().Bool("health-check-enabled", false, "Enable health checks") cmd.Flags().String("health-check-path", "", "Health check path") + cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage") return cmd } diff --git a/cmd/application/create/github.go b/cmd/application/create/github.go index 291f2f6..dd8c4ad 100644 --- a/cmd/application/create/github.go +++ b/cmd/application/create/github.go @@ -94,6 +94,7 @@ Examples: setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy) setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled) setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath) + setOptionalStringFlag(cmd, "dockerfile-target-build", &req.DockerfileTargetBuild) client, err := cli.GetAPIClient(cmd) if err != nil { @@ -148,6 +149,7 @@ Examples: cmd.Flags().String("limits-memory", "", "Memory limit") cmd.Flags().Bool("health-check-enabled", false, "Enable health checks") cmd.Flags().String("health-check-path", "", "Health check path") + cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage") return cmd } diff --git a/cmd/application/create/public.go b/cmd/application/create/public.go index a6c5cb7..3dd0805 100644 --- a/cmd/application/create/public.go +++ b/cmd/application/create/public.go @@ -85,6 +85,7 @@ Examples: setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy) setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled) setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath) + setOptionalStringFlag(cmd, "dockerfile-target-build", &req.DockerfileTargetBuild) client, err := cli.GetAPIClient(cmd) if err != nil { @@ -138,6 +139,7 @@ Examples: cmd.Flags().String("limits-memory", "", "Memory limit") cmd.Flags().Bool("health-check-enabled", false, "Enable health checks") cmd.Flags().String("health-check-path", "", "Health check path") + cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage") return cmd } diff --git a/cmd/application/storage/create.go b/cmd/application/storage/create.go new file mode 100644 index 0000000..1579f7a --- /dev/null +++ b/cmd/application/storage/create.go @@ -0,0 +1,92 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/models" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewCreateCommand returns the storage create command +func NewCreateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a storage for an application", + Long: `Create a persistent volume or file storage for an application. + +Examples: + coolify app storage create --type persistent --name my-volume --mount-path /data + coolify app storage create --type persistent --name my-volume --mount-path /data --host-path /var/data + coolify app storage create --type file --mount-path /app/config.yml --content "key: value" + coolify app storage create --type file --mount-path /app/data --is-directory --fs-path /app/data`, + Args: cli.ExactArgs(1, ""), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + storageType, _ := cmd.Flags().GetString("type") + mountPath, _ := cmd.Flags().GetString("mount-path") + + if storageType == "" { + return fmt.Errorf("--type is required (persistent or file)") + } + if storageType != "persistent" && storageType != "file" { + return fmt.Errorf("--type must be 'persistent' or 'file'") + } + if mountPath == "" { + return fmt.Errorf("--mount-path is required") + } + + req := &models.StorageCreateRequest{ + Type: storageType, + MountPath: mountPath, + } + + if cmd.Flags().Changed("name") { + val, _ := cmd.Flags().GetString("name") + req.Name = &val + } + if cmd.Flags().Changed("host-path") { + val, _ := cmd.Flags().GetString("host-path") + req.HostPath = &val + } + if cmd.Flags().Changed("content") { + val, _ := cmd.Flags().GetString("content") + req.Content = &val + } + if cmd.Flags().Changed("is-directory") { + val, _ := cmd.Flags().GetBool("is-directory") + req.IsDirectory = &val + } + if cmd.Flags().Changed("fs-path") { + val, _ := cmd.Flags().GetString("fs-path") + req.FsPath = &val + } + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + appSvc := service.NewApplicationService(client) + if err := appSvc.CreateStorage(ctx, args[0], req); err != nil { + return fmt.Errorf("failed to create storage: %w", err) + } + + fmt.Println("Storage created successfully.") + return nil + }, + } + + cmd.Flags().String("type", "", "Storage type: 'persistent' or 'file' (required)") + cmd.Flags().String("mount-path", "", "Mount path inside the container (required)") + cmd.Flags().String("name", "", "Volume name (persistent only)") + cmd.Flags().String("host-path", "", "Host path (persistent only)") + cmd.Flags().String("content", "", "File content (file only)") + cmd.Flags().Bool("is-directory", false, "Whether this is a directory mount (file only)") + cmd.Flags().String("fs-path", "", "Host directory path (file only, required when --is-directory is set)") + + return cmd +} diff --git a/cmd/application/storage/delete.go b/cmd/application/storage/delete.go new file mode 100644 index 0000000..6a9c663 --- /dev/null +++ b/cmd/application/storage/delete.go @@ -0,0 +1,39 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewDeleteCommand returns the storage delete command +func NewDeleteCommand() *cobra.Command { + return &cobra.Command{ + Use: "delete ", + Short: "Delete a storage from an application", + Long: `Delete a persistent volume or file storage from an application. + +Examples: + coolify app storage delete `, + Args: cli.ExactArgs(2, " "), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + appSvc := service.NewApplicationService(client) + if err := appSvc.DeleteStorage(ctx, args[0], args[1]); err != nil { + return fmt.Errorf("failed to delete storage: %w", err) + } + + fmt.Println("Storage deleted successfully.") + return nil + }, + } +} diff --git a/cmd/application/storage/list.go b/cmd/application/storage/list.go new file mode 100644 index 0000000..2a879a8 --- /dev/null +++ b/cmd/application/storage/list.go @@ -0,0 +1,47 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/output" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewListCommand returns the storage list command +func NewListCommand() *cobra.Command { + return &cobra.Command{ + Use: "list ", + Short: "List all storages for an application", + Long: `List all persistent volumes and file storages for a specific application.`, + Args: cli.ExactArgs(1, ""), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + appSvc := service.NewApplicationService(client) + storages, err := appSvc.ListStorages(ctx, args[0]) + if err != nil { + return fmt.Errorf("failed to list storages: %w", err) + } + + format, _ := cmd.Flags().GetString("format") + showSensitive, _ := cmd.Flags().GetBool("show-sensitive") + + formatter, err := output.NewFormatter(format, output.Options{ + ShowSensitive: showSensitive, + }) + if err != nil { + return err + } + + return formatter.Format(storages) + }, + } +} diff --git a/cmd/application/storage/update.go b/cmd/application/storage/update.go new file mode 100644 index 0000000..29d1c57 --- /dev/null +++ b/cmd/application/storage/update.go @@ -0,0 +1,113 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/models" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewUpdateCommand returns the storage update command +func NewUpdateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a storage for an application", + Long: `Update a persistent volume or file storage for an application. + +The --uuid and --type flags are required. Use 'coolify app storage list' to find storage UUIDs. + +For read-only storages (from docker-compose or services), only --is-preview-suffix-enabled can be updated. + +Examples: + coolify app storage update --uuid --type persistent --name my-volume --mount-path /data + coolify app storage update --uuid --type file --content "config content" --mount-path /app/config.yml + coolify app storage update --uuid --type persistent --is-preview-suffix-enabled`, + Args: cli.ExactArgs(1, ""), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + storageUUID, _ := cmd.Flags().GetString("uuid") + storageID, _ := cmd.Flags().GetInt("id") + storageType, _ := cmd.Flags().GetString("type") + + if storageUUID == "" && storageID == 0 { + return fmt.Errorf("--uuid is required (or --id as deprecated fallback)") + } + if storageType == "" { + return fmt.Errorf("--type is required (persistent or file)") + } + if storageType != "persistent" && storageType != "file" { + return fmt.Errorf("--type must be 'persistent' or 'file'") + } + + req := &models.StorageUpdateRequest{ + Type: storageType, + } + + if storageUUID != "" { + req.UUID = &storageUUID + } else { + req.ID = &storageID + } + + hasUpdates := false + + if cmd.Flags().Changed("is-preview-suffix-enabled") { + val, _ := cmd.Flags().GetBool("is-preview-suffix-enabled") + req.IsPreviewSuffixEnabled = &val + hasUpdates = true + } + if cmd.Flags().Changed("name") { + val, _ := cmd.Flags().GetString("name") + req.Name = &val + hasUpdates = true + } + if cmd.Flags().Changed("mount-path") { + val, _ := cmd.Flags().GetString("mount-path") + req.MountPath = &val + hasUpdates = true + } + if cmd.Flags().Changed("host-path") { + val, _ := cmd.Flags().GetString("host-path") + req.HostPath = &val + hasUpdates = true + } + if cmd.Flags().Changed("content") { + val, _ := cmd.Flags().GetString("content") + req.Content = &val + hasUpdates = true + } + + if !hasUpdates { + return fmt.Errorf("no fields to update. Use --help to see available flags") + } + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + appSvc := service.NewApplicationService(client) + if err := appSvc.UpdateStorage(ctx, args[0], req); err != nil { + return fmt.Errorf("failed to update storage: %w", err) + } + + fmt.Println("Storage updated successfully.") + return nil + }, + } + + cmd.Flags().String("uuid", "", "Storage UUID (required, use 'storage list' to find)") + cmd.Flags().Int("id", 0, "Storage ID (deprecated, use --uuid instead)") + cmd.Flags().String("type", "", "Storage type: 'persistent' or 'file' (required)") + cmd.Flags().Bool("is-preview-suffix-enabled", false, "Enable preview suffix for this storage") + cmd.Flags().String("name", "", "Storage name (persistent only)") + cmd.Flags().String("mount-path", "", "Mount path inside the container") + cmd.Flags().String("host-path", "", "Host path (persistent only)") + cmd.Flags().String("content", "", "File content (file only)") + + return cmd +} diff --git a/cmd/application/update.go b/cmd/application/update.go index e360ad6..8804b4d 100644 --- a/cmd/application/update.go +++ b/cmd/application/update.go @@ -104,6 +104,11 @@ func NewUpdateCommand() *cobra.Command { req.PortsMappings = &ports hasUpdates = true } + if cmd.Flags().Changed("dockerfile-target-build") { + targetBuild, _ := cmd.Flags().GetString("dockerfile-target-build") + req.DockerfileTargetBuild = &targetBuild + hasUpdates = true + } if cmd.Flags().Changed("health-check-enabled") { enabled, _ := cmd.Flags().GetBool("health-check-enabled") req.HealthCheckEnabled = &enabled @@ -152,6 +157,7 @@ func NewUpdateCommand() *cobra.Command { cmd.Flags().String("dockerfile", "", "Dockerfile content") cmd.Flags().String("docker-image", "", "Docker image name") cmd.Flags().String("docker-tag", "", "Docker image tag") + cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage") cmd.Flags().String("ports-exposes", "", "Exposed ports") cmd.Flags().String("ports-mappings", "", "Port mappings") cmd.Flags().Bool("health-check-enabled", false, "Enable health check") diff --git a/cmd/database/database.go b/cmd/database/database.go index 06c42d9..6378735 100644 --- a/cmd/database/database.go +++ b/cmd/database/database.go @@ -5,6 +5,7 @@ import ( "github.com/coollabsio/coolify-cli/cmd/database/backup" "github.com/coollabsio/coolify-cli/cmd/database/env" + "github.com/coollabsio/coolify-cli/cmd/database/storage" ) // NewDatabaseCommand creates the database parent command with all subcommands @@ -53,5 +54,18 @@ func NewDatabaseCommand() *cobra.Command { backupCmd.AddCommand(backup.NewDeleteExecutionCommand()) cmd.AddCommand(backupCmd) + // Add storage subcommand + storageCmd := &cobra.Command{ + Use: "storage", + Aliases: []string{"storages"}, + Short: "Manage database storages", + Long: `List and manage persistent volumes and file storages for databases.`, + } + storageCmd.AddCommand(storage.NewListCommand()) + storageCmd.AddCommand(storage.NewCreateCommand()) + storageCmd.AddCommand(storage.NewUpdateCommand()) + storageCmd.AddCommand(storage.NewDeleteCommand()) + cmd.AddCommand(storageCmd) + return cmd } diff --git a/cmd/database/storage/create.go b/cmd/database/storage/create.go new file mode 100644 index 0000000..ec0555d --- /dev/null +++ b/cmd/database/storage/create.go @@ -0,0 +1,90 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/models" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewCreateCommand returns the database storage create command +func NewCreateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a storage for a database", + Long: `Create a persistent volume or file storage for a database. + +Examples: + coolify db storage create --type persistent --name my-volume --mount-path /data + coolify db storage create --type file --mount-path /app/config.yml --content "key: value"`, + Args: cli.ExactArgs(1, ""), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + storageType, _ := cmd.Flags().GetString("type") + mountPath, _ := cmd.Flags().GetString("mount-path") + + if storageType == "" { + return fmt.Errorf("--type is required (persistent or file)") + } + if storageType != "persistent" && storageType != "file" { + return fmt.Errorf("--type must be 'persistent' or 'file'") + } + if mountPath == "" { + return fmt.Errorf("--mount-path is required") + } + + req := &models.StorageCreateRequest{ + Type: storageType, + MountPath: mountPath, + } + + if cmd.Flags().Changed("name") { + val, _ := cmd.Flags().GetString("name") + req.Name = &val + } + if cmd.Flags().Changed("host-path") { + val, _ := cmd.Flags().GetString("host-path") + req.HostPath = &val + } + if cmd.Flags().Changed("content") { + val, _ := cmd.Flags().GetString("content") + req.Content = &val + } + if cmd.Flags().Changed("is-directory") { + val, _ := cmd.Flags().GetBool("is-directory") + req.IsDirectory = &val + } + if cmd.Flags().Changed("fs-path") { + val, _ := cmd.Flags().GetString("fs-path") + req.FsPath = &val + } + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + dbSvc := service.NewDatabaseService(client) + if err := dbSvc.CreateStorage(ctx, args[0], req); err != nil { + return fmt.Errorf("failed to create storage: %w", err) + } + + fmt.Println("Storage created successfully.") + return nil + }, + } + + cmd.Flags().String("type", "", "Storage type: 'persistent' or 'file' (required)") + cmd.Flags().String("mount-path", "", "Mount path inside the container (required)") + cmd.Flags().String("name", "", "Volume name (persistent only)") + cmd.Flags().String("host-path", "", "Host path (persistent only)") + cmd.Flags().String("content", "", "File content (file only)") + cmd.Flags().Bool("is-directory", false, "Whether this is a directory mount (file only)") + cmd.Flags().String("fs-path", "", "Host directory path (file only, required when --is-directory is set)") + + return cmd +} diff --git a/cmd/database/storage/delete.go b/cmd/database/storage/delete.go new file mode 100644 index 0000000..eb4e93a --- /dev/null +++ b/cmd/database/storage/delete.go @@ -0,0 +1,39 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewDeleteCommand returns the database storage delete command +func NewDeleteCommand() *cobra.Command { + return &cobra.Command{ + Use: "delete ", + Short: "Delete a storage from a database", + Long: `Delete a persistent volume or file storage from a database. + +Examples: + coolify db storage delete `, + Args: cli.ExactArgs(2, " "), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + dbSvc := service.NewDatabaseService(client) + if err := dbSvc.DeleteStorage(ctx, args[0], args[1]); err != nil { + return fmt.Errorf("failed to delete storage: %w", err) + } + + fmt.Println("Storage deleted successfully.") + return nil + }, + } +} diff --git a/cmd/database/storage/list.go b/cmd/database/storage/list.go new file mode 100644 index 0000000..26981ed --- /dev/null +++ b/cmd/database/storage/list.go @@ -0,0 +1,47 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/output" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewListCommand returns the database storage list command +func NewListCommand() *cobra.Command { + return &cobra.Command{ + Use: "list ", + Short: "List all storages for a database", + Long: `List all persistent volumes and file storages for a specific database.`, + Args: cli.ExactArgs(1, ""), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + dbSvc := service.NewDatabaseService(client) + storages, err := dbSvc.ListStorages(ctx, args[0]) + if err != nil { + return fmt.Errorf("failed to list storages: %w", err) + } + + format, _ := cmd.Flags().GetString("format") + showSensitive, _ := cmd.Flags().GetBool("show-sensitive") + + formatter, err := output.NewFormatter(format, output.Options{ + ShowSensitive: showSensitive, + }) + if err != nil { + return err + } + + return formatter.Format(storages) + }, + } +} diff --git a/cmd/database/storage/update.go b/cmd/database/storage/update.go new file mode 100644 index 0000000..3409f26 --- /dev/null +++ b/cmd/database/storage/update.go @@ -0,0 +1,110 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/models" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewUpdateCommand returns the database storage update command +func NewUpdateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a storage for a database", + Long: `Update a persistent volume or file storage for a database. + +The --uuid and --type flags are required. Use 'coolify db storage list' to find storage UUIDs. + +Examples: + coolify db storage update --uuid --type persistent --name my-volume + coolify db storage update --uuid --type persistent --is-preview-suffix-enabled`, + Args: cli.ExactArgs(1, ""), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + storageUUID, _ := cmd.Flags().GetString("uuid") + storageID, _ := cmd.Flags().GetInt("id") + storageType, _ := cmd.Flags().GetString("type") + + if storageUUID == "" && storageID == 0 { + return fmt.Errorf("--uuid is required (or --id as deprecated fallback)") + } + if storageType == "" { + return fmt.Errorf("--type is required (persistent or file)") + } + if storageType != "persistent" && storageType != "file" { + return fmt.Errorf("--type must be 'persistent' or 'file'") + } + + req := &models.StorageUpdateRequest{ + Type: storageType, + } + + if storageUUID != "" { + req.UUID = &storageUUID + } else { + req.ID = &storageID + } + + hasUpdates := false + + if cmd.Flags().Changed("is-preview-suffix-enabled") { + val, _ := cmd.Flags().GetBool("is-preview-suffix-enabled") + req.IsPreviewSuffixEnabled = &val + hasUpdates = true + } + if cmd.Flags().Changed("name") { + val, _ := cmd.Flags().GetString("name") + req.Name = &val + hasUpdates = true + } + if cmd.Flags().Changed("mount-path") { + val, _ := cmd.Flags().GetString("mount-path") + req.MountPath = &val + hasUpdates = true + } + if cmd.Flags().Changed("host-path") { + val, _ := cmd.Flags().GetString("host-path") + req.HostPath = &val + hasUpdates = true + } + if cmd.Flags().Changed("content") { + val, _ := cmd.Flags().GetString("content") + req.Content = &val + hasUpdates = true + } + + if !hasUpdates { + return fmt.Errorf("no fields to update. Use --help to see available flags") + } + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + dbSvc := service.NewDatabaseService(client) + if err := dbSvc.UpdateStorage(ctx, args[0], req); err != nil { + return fmt.Errorf("failed to update storage: %w", err) + } + + fmt.Println("Storage updated successfully.") + return nil + }, + } + + cmd.Flags().String("uuid", "", "Storage UUID (required, use 'storage list' to find)") + cmd.Flags().Int("id", 0, "Storage ID (deprecated, use --uuid instead)") + cmd.Flags().String("type", "", "Storage type: 'persistent' or 'file' (required)") + cmd.Flags().Bool("is-preview-suffix-enabled", false, "Enable preview suffix for this storage") + cmd.Flags().String("name", "", "Storage name (persistent only)") + cmd.Flags().String("mount-path", "", "Mount path inside the container") + cmd.Flags().String("host-path", "", "Host path (persistent only)") + cmd.Flags().String("content", "", "File content (file only)") + + return cmd +} diff --git a/cmd/service/service.go b/cmd/service/service.go index c14b162..67141bf 100644 --- a/cmd/service/service.go +++ b/cmd/service/service.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/coollabsio/coolify-cli/cmd/service/env" + "github.com/coollabsio/coolify-cli/cmd/service/storage" ) // NewServiceCommand creates the service parent command with all subcommands @@ -37,5 +38,18 @@ func NewServiceCommand() *cobra.Command { envCmd.AddCommand(env.NewSyncCommand()) cmd.AddCommand(envCmd) + // Add storage subcommand + storageCmd := &cobra.Command{ + Use: "storage", + Aliases: []string{"storages"}, + Short: "Manage service storages", + Long: `List and manage persistent volumes and file storages for services.`, + } + storageCmd.AddCommand(storage.NewListCommand()) + storageCmd.AddCommand(storage.NewCreateCommand()) + storageCmd.AddCommand(storage.NewUpdateCommand()) + storageCmd.AddCommand(storage.NewDeleteCommand()) + cmd.AddCommand(storageCmd) + return cmd } diff --git a/cmd/service/storage/create.go b/cmd/service/storage/create.go new file mode 100644 index 0000000..3e423f3 --- /dev/null +++ b/cmd/service/storage/create.go @@ -0,0 +1,99 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/models" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewCreateCommand returns the service storage create command +func NewCreateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a storage for a service", + Long: `Create a persistent volume or file storage for a service. + +The --resource-uuid flag is required to specify which service sub-resource +(application or database) the storage belongs to. + +Examples: + coolify svc storage create --resource-uuid --type persistent --name my-volume --mount-path /data + coolify svc storage create --resource-uuid --type file --mount-path /app/config.yml --content "key: value"`, + Args: cli.ExactArgs(1, ""), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + storageType, _ := cmd.Flags().GetString("type") + mountPath, _ := cmd.Flags().GetString("mount-path") + resourceUUID, _ := cmd.Flags().GetString("resource-uuid") + + if storageType == "" { + return fmt.Errorf("--type is required (persistent or file)") + } + if storageType != "persistent" && storageType != "file" { + return fmt.Errorf("--type must be 'persistent' or 'file'") + } + if mountPath == "" { + return fmt.Errorf("--mount-path is required") + } + if resourceUUID == "" { + return fmt.Errorf("--resource-uuid is required (UUID of the service sub-resource)") + } + + req := &models.ServiceStorageCreateRequest{ + Type: storageType, + MountPath: mountPath, + ResourceUUID: resourceUUID, + } + + if cmd.Flags().Changed("name") { + val, _ := cmd.Flags().GetString("name") + req.Name = &val + } + if cmd.Flags().Changed("host-path") { + val, _ := cmd.Flags().GetString("host-path") + req.HostPath = &val + } + if cmd.Flags().Changed("content") { + val, _ := cmd.Flags().GetString("content") + req.Content = &val + } + if cmd.Flags().Changed("is-directory") { + val, _ := cmd.Flags().GetBool("is-directory") + req.IsDirectory = &val + } + if cmd.Flags().Changed("fs-path") { + val, _ := cmd.Flags().GetString("fs-path") + req.FsPath = &val + } + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + svcSvc := service.NewService(client) + if err := svcSvc.CreateStorage(ctx, args[0], req); err != nil { + return fmt.Errorf("failed to create storage: %w", err) + } + + fmt.Println("Storage created successfully.") + return nil + }, + } + + cmd.Flags().String("type", "", "Storage type: 'persistent' or 'file' (required)") + cmd.Flags().String("mount-path", "", "Mount path inside the container (required)") + cmd.Flags().String("resource-uuid", "", "UUID of the service sub-resource (required)") + cmd.Flags().String("name", "", "Volume name (persistent only)") + cmd.Flags().String("host-path", "", "Host path (persistent only)") + cmd.Flags().String("content", "", "File content (file only)") + cmd.Flags().Bool("is-directory", false, "Whether this is a directory mount (file only)") + cmd.Flags().String("fs-path", "", "Host directory path (file only, required when --is-directory is set)") + + return cmd +} diff --git a/cmd/service/storage/delete.go b/cmd/service/storage/delete.go new file mode 100644 index 0000000..0206f95 --- /dev/null +++ b/cmd/service/storage/delete.go @@ -0,0 +1,39 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewDeleteCommand returns the service storage delete command +func NewDeleteCommand() *cobra.Command { + return &cobra.Command{ + Use: "delete ", + Short: "Delete a storage from a service", + Long: `Delete a persistent volume or file storage from a service. + +Examples: + coolify svc storage delete `, + Args: cli.ExactArgs(2, " "), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + svcSvc := service.NewService(client) + if err := svcSvc.DeleteStorage(ctx, args[0], args[1]); err != nil { + return fmt.Errorf("failed to delete storage: %w", err) + } + + fmt.Println("Storage deleted successfully.") + return nil + }, + } +} diff --git a/cmd/service/storage/list.go b/cmd/service/storage/list.go new file mode 100644 index 0000000..8ba3313 --- /dev/null +++ b/cmd/service/storage/list.go @@ -0,0 +1,47 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/output" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewListCommand returns the service storage list command +func NewListCommand() *cobra.Command { + return &cobra.Command{ + Use: "list ", + Short: "List all storages for a service", + Long: `List all persistent volumes and file storages for a specific service.`, + Args: cli.ExactArgs(1, ""), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + svcSvc := service.NewService(client) + storages, err := svcSvc.ListStorages(ctx, args[0]) + if err != nil { + return fmt.Errorf("failed to list storages: %w", err) + } + + format, _ := cmd.Flags().GetString("format") + showSensitive, _ := cmd.Flags().GetBool("show-sensitive") + + formatter, err := output.NewFormatter(format, output.Options{ + ShowSensitive: showSensitive, + }) + if err != nil { + return err + } + + return formatter.Format(storages) + }, + } +} diff --git a/cmd/service/storage/update.go b/cmd/service/storage/update.go new file mode 100644 index 0000000..6f4c947 --- /dev/null +++ b/cmd/service/storage/update.go @@ -0,0 +1,110 @@ +package storage + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/cli" + "github.com/coollabsio/coolify-cli/internal/models" + "github.com/coollabsio/coolify-cli/internal/service" +) + +// NewUpdateCommand returns the service storage update command +func NewUpdateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a storage for a service", + Long: `Update a persistent volume or file storage for a service. + +The --uuid and --type flags are required. Use 'coolify svc storage list' to find storage UUIDs. + +Examples: + coolify svc storage update --uuid --type persistent --name my-volume + coolify svc storage update --uuid --type persistent --is-preview-suffix-enabled`, + Args: cli.ExactArgs(1, ""), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + storageUUID, _ := cmd.Flags().GetString("uuid") + storageID, _ := cmd.Flags().GetInt("id") + storageType, _ := cmd.Flags().GetString("type") + + if storageUUID == "" && storageID == 0 { + return fmt.Errorf("--uuid is required (or --id as deprecated fallback)") + } + if storageType == "" { + return fmt.Errorf("--type is required (persistent or file)") + } + if storageType != "persistent" && storageType != "file" { + return fmt.Errorf("--type must be 'persistent' or 'file'") + } + + req := &models.StorageUpdateRequest{ + Type: storageType, + } + + if storageUUID != "" { + req.UUID = &storageUUID + } else { + req.ID = &storageID + } + + hasUpdates := false + + if cmd.Flags().Changed("is-preview-suffix-enabled") { + val, _ := cmd.Flags().GetBool("is-preview-suffix-enabled") + req.IsPreviewSuffixEnabled = &val + hasUpdates = true + } + if cmd.Flags().Changed("name") { + val, _ := cmd.Flags().GetString("name") + req.Name = &val + hasUpdates = true + } + if cmd.Flags().Changed("mount-path") { + val, _ := cmd.Flags().GetString("mount-path") + req.MountPath = &val + hasUpdates = true + } + if cmd.Flags().Changed("host-path") { + val, _ := cmd.Flags().GetString("host-path") + req.HostPath = &val + hasUpdates = true + } + if cmd.Flags().Changed("content") { + val, _ := cmd.Flags().GetString("content") + req.Content = &val + hasUpdates = true + } + + if !hasUpdates { + return fmt.Errorf("no fields to update. Use --help to see available flags") + } + + client, err := cli.GetAPIClient(cmd) + if err != nil { + return fmt.Errorf("failed to get API client: %w", err) + } + + svcSvc := service.NewService(client) + if err := svcSvc.UpdateStorage(ctx, args[0], req); err != nil { + return fmt.Errorf("failed to update storage: %w", err) + } + + fmt.Println("Storage updated successfully.") + return nil + }, + } + + cmd.Flags().String("uuid", "", "Storage UUID (required, use 'storage list' to find)") + cmd.Flags().Int("id", 0, "Storage ID (deprecated, use --uuid instead)") + cmd.Flags().String("type", "", "Storage type: 'persistent' or 'file' (required)") + cmd.Flags().Bool("is-preview-suffix-enabled", false, "Enable preview suffix for this storage") + cmd.Flags().String("name", "", "Storage name (persistent only)") + cmd.Flags().String("mount-path", "", "Mount path inside the container") + cmd.Flags().String("host-path", "", "Host path (persistent only)") + cmd.Flags().String("content", "", "File content (file only)") + + return cmd +} diff --git a/internal/models/application.go b/internal/models/application.go index 208003d..113221d 100644 --- a/internal/models/application.go +++ b/internal/models/application.go @@ -43,6 +43,7 @@ type ApplicationUpdateRequest struct { // Docker configuration Dockerfile *string `json:"dockerfile,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` DockerRegistryImageName *string `json:"docker_registry_image_name,omitempty"` DockerRegistryImageTag *string `json:"docker_registry_image_tag,omitempty"` CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` @@ -137,6 +138,130 @@ type EnvironmentVariableUpdateRequest struct { Comment *string `json:"comment,omitempty"` } +// StoragesResponse represents the API response for listing storages +type StoragesResponse struct { + PersistentStorages []PersistentStorage `json:"persistent_storages"` + FileStorages []FileStorage `json:"file_storages"` +} + +// PersistentStorage represents a persistent volume for an application +type PersistentStorage struct { + ID int `json:"id" table:"-"` + UUID string `json:"uuid"` + Name string `json:"name"` + MountPath string `json:"mount_path"` + HostPath *string `json:"host_path,omitempty"` + IsPreviewSuffixEnabled bool `json:"is_preview_suffix_enabled"` + IsReadOnly bool `json:"is_readonly"` + ResourceType string `json:"resource_type" table:"-"` + ResourceID int `json:"resource_id" table:"-"` +} + +// FileStorage represents a file storage for an application +type FileStorage struct { + ID int `json:"id"` + UUID string `json:"uuid"` + FsPath string `json:"fs_path"` + MountPath string `json:"mount_path"` + Content *string `json:"content,omitempty"` + IsDirectory bool `json:"is_directory"` + IsBasedOnGit bool `json:"is_based_on_git"` + IsPreviewSuffixEnabled bool `json:"is_preview_suffix_enabled"` + Chown *string `json:"chown,omitempty"` + Chmod *string `json:"chmod,omitempty"` + ResourceType string `json:"resource_type" table:"-"` + ResourceID int `json:"resource_id" table:"-"` +} + +// StorageListItem is a unified view of both storage types for table output +type StorageListItem struct { + ID int `json:"id" table:"-"` + UUID string `json:"uuid"` + Type string `json:"type"` + Name string `json:"name"` + MountPath string `json:"mount_path"` + HostPath string `json:"host_path,omitempty"` + IsPreviewSuffixEnabled bool `json:"is_preview_suffix_enabled"` + Content string `json:"content,omitempty" table:"-"` +} + +// MergeStorages converts a StoragesResponse into a unified list of StorageListItem +func MergeStorages(resp StoragesResponse) []StorageListItem { + var items []StorageListItem + for _, ps := range resp.PersistentStorages { + hostPath := "" + if ps.HostPath != nil { + hostPath = *ps.HostPath + } + items = append(items, StorageListItem{ + ID: ps.ID, + UUID: ps.UUID, + Type: "persistent", + Name: ps.Name, + MountPath: ps.MountPath, + HostPath: hostPath, + IsPreviewSuffixEnabled: ps.IsPreviewSuffixEnabled, + }) + } + for _, fs := range resp.FileStorages { + content := "" + if fs.Content != nil { + content = *fs.Content + } + items = append(items, StorageListItem{ + ID: fs.ID, + UUID: fs.UUID, + Type: "file", + Name: fs.FsPath, + MountPath: fs.MountPath, + IsPreviewSuffixEnabled: fs.IsPreviewSuffixEnabled, + Content: content, + }) + } + return items +} + +// StorageCreateRequest represents the request to create a storage for applications and databases +type StorageCreateRequest struct { + Type string `json:"type"` // "persistent" or "file" + MountPath string `json:"mount_path"` // required + Name *string `json:"name,omitempty"` + HostPath *string `json:"host_path,omitempty"` + Content *string `json:"content,omitempty"` + IsDirectory *bool `json:"is_directory,omitempty"` + FsPath *string `json:"fs_path,omitempty"` +} + +// ServiceStorageCreateRequest represents the request to create a storage for services +// Services require resource_uuid to identify which sub-resource the storage belongs to +type ServiceStorageCreateRequest struct { + Type string `json:"type"` // "persistent" or "file" + MountPath string `json:"mount_path"` // required + ResourceUUID string `json:"resource_uuid"` // required for services + Name *string `json:"name,omitempty"` + HostPath *string `json:"host_path,omitempty"` + Content *string `json:"content,omitempty"` + IsDirectory *bool `json:"is_directory,omitempty"` + FsPath *string `json:"fs_path,omitempty"` +} + +// StorageUpdateRequest represents the request to update a storage +type StorageUpdateRequest struct { + // Required fields + Type string `json:"type"` // "persistent" or "file" + + // Identifier (uuid preferred, id deprecated) + UUID *string `json:"uuid,omitempty"` + ID *int `json:"id,omitempty"` + + // Optional fields + IsPreviewSuffixEnabled *bool `json:"is_preview_suffix_enabled,omitempty"` + Name *string `json:"name,omitempty"` + MountPath *string `json:"mount_path,omitempty"` + HostPath *string `json:"host_path,omitempty"` + Content *string `json:"content,omitempty"` +} + // ApplicationCreatePublicRequest for POST /applications/public // Creates an application from a public git repository type ApplicationCreatePublicRequest struct { @@ -165,8 +290,9 @@ type ApplicationCreatePublicRequest struct { BaseDirectory *string `json:"base_directory,omitempty"` PublishDirectory *string `json:"publish_directory,omitempty"` PortsMappings *string `json:"ports_mappings,omitempty"` - CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` - CustomLabels *string `json:"custom_labels,omitempty"` + CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` + CustomLabels *string `json:"custom_labels,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` // Health checks HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` @@ -208,14 +334,15 @@ type ApplicationCreateGitHubAppRequest struct { BaseDirectory *string `json:"base_directory,omitempty"` PublishDirectory *string `json:"publish_directory,omitempty"` PortsMappings *string `json:"ports_mappings,omitempty"` - CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` - CustomLabels *string `json:"custom_labels,omitempty"` - HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` - HealthCheckPath *string `json:"health_check_path,omitempty"` - HealthCheckPort *string `json:"health_check_port,omitempty"` - HealthCheckMethod *string `json:"health_check_method,omitempty"` - LimitsCPUs *string `json:"limits_cpus,omitempty"` - LimitsMemory *string `json:"limits_memory,omitempty"` + CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` + CustomLabels *string `json:"custom_labels,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` + HealthCheckPath *string `json:"health_check_path,omitempty"` + HealthCheckPort *string `json:"health_check_port,omitempty"` + HealthCheckMethod *string `json:"health_check_method,omitempty"` + LimitsCPUs *string `json:"limits_cpus,omitempty"` + LimitsMemory *string `json:"limits_memory,omitempty"` } // ApplicationCreateDeployKeyRequest for POST /applications/private-deploy-key @@ -247,14 +374,15 @@ type ApplicationCreateDeployKeyRequest struct { BaseDirectory *string `json:"base_directory,omitempty"` PublishDirectory *string `json:"publish_directory,omitempty"` PortsMappings *string `json:"ports_mappings,omitempty"` - CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` - CustomLabels *string `json:"custom_labels,omitempty"` - HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` - HealthCheckPath *string `json:"health_check_path,omitempty"` - HealthCheckPort *string `json:"health_check_port,omitempty"` - HealthCheckMethod *string `json:"health_check_method,omitempty"` - LimitsCPUs *string `json:"limits_cpus,omitempty"` - LimitsMemory *string `json:"limits_memory,omitempty"` + CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` + CustomLabels *string `json:"custom_labels,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` + HealthCheckPath *string `json:"health_check_path,omitempty"` + HealthCheckPort *string `json:"health_check_port,omitempty"` + HealthCheckMethod *string `json:"health_check_method,omitempty"` + LimitsCPUs *string `json:"limits_cpus,omitempty"` + LimitsMemory *string `json:"limits_memory,omitempty"` } // ApplicationCreateDockerfileRequest for POST /applications/dockerfile @@ -277,14 +405,15 @@ type ApplicationCreateDockerfileRequest struct { DestinationUUID *string `json:"destination_uuid,omitempty"` PortsExposes *string `json:"ports_exposes,omitempty"` PortsMappings *string `json:"ports_mappings,omitempty"` - CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` - CustomLabels *string `json:"custom_labels,omitempty"` - HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` - HealthCheckPath *string `json:"health_check_path,omitempty"` - HealthCheckPort *string `json:"health_check_port,omitempty"` - HealthCheckMethod *string `json:"health_check_method,omitempty"` - LimitsCPUs *string `json:"limits_cpus,omitempty"` - LimitsMemory *string `json:"limits_memory,omitempty"` + CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` + CustomLabels *string `json:"custom_labels,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` + HealthCheckPath *string `json:"health_check_path,omitempty"` + HealthCheckPort *string `json:"health_check_port,omitempty"` + HealthCheckMethod *string `json:"health_check_method,omitempty"` + LimitsCPUs *string `json:"limits_cpus,omitempty"` + LimitsMemory *string `json:"limits_memory,omitempty"` } // ApplicationCreateDockerImageRequest for POST /applications/dockerimage @@ -308,12 +437,13 @@ type ApplicationCreateDockerImageRequest struct { DestinationUUID *string `json:"destination_uuid,omitempty"` DockerRegistryImageTag *string `json:"docker_registry_image_tag,omitempty"` PortsMappings *string `json:"ports_mappings,omitempty"` - CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` - CustomLabels *string `json:"custom_labels,omitempty"` - HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` - HealthCheckPath *string `json:"health_check_path,omitempty"` - HealthCheckPort *string `json:"health_check_port,omitempty"` - HealthCheckMethod *string `json:"health_check_method,omitempty"` - LimitsCPUs *string `json:"limits_cpus,omitempty"` - LimitsMemory *string `json:"limits_memory,omitempty"` + CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` + CustomLabels *string `json:"custom_labels,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` + HealthCheckPath *string `json:"health_check_path,omitempty"` + HealthCheckPort *string `json:"health_check_port,omitempty"` + HealthCheckMethod *string `json:"health_check_method,omitempty"` + LimitsCPUs *string `json:"limits_cpus,omitempty"` + LimitsMemory *string `json:"limits_memory,omitempty"` } diff --git a/internal/service/application.go b/internal/service/application.go index a306275..89f4139 100644 --- a/internal/service/application.go +++ b/internal/service/application.go @@ -196,6 +196,43 @@ func (s *ApplicationService) BulkUpdateEnvs(ctx context.Context, appUUID string, return &response, nil } +// ListStorages retrieves all storages for an application +func (s *ApplicationService) ListStorages(ctx context.Context, uuid string) ([]models.StorageListItem, error) { + var resp models.StoragesResponse + err := s.client.Get(ctx, fmt.Sprintf("applications/%s/storages", uuid), &resp) + if err != nil { + return nil, fmt.Errorf("failed to list storages for application %s: %w", uuid, err) + } + return models.MergeStorages(resp), nil +} + +// CreateStorage creates a new storage for an application +func (s *ApplicationService) CreateStorage(ctx context.Context, uuid string, req *models.StorageCreateRequest) error { + err := s.client.Post(ctx, fmt.Sprintf("applications/%s/storages", uuid), req, nil) + if err != nil { + return fmt.Errorf("failed to create storage for application %s: %w", uuid, err) + } + return nil +} + +// UpdateStorage updates a storage for an application +func (s *ApplicationService) UpdateStorage(ctx context.Context, uuid string, req *models.StorageUpdateRequest) error { + err := s.client.Patch(ctx, fmt.Sprintf("applications/%s/storages", uuid), req, nil) + if err != nil { + return fmt.Errorf("failed to update storage for application %s: %w", uuid, err) + } + return nil +} + +// DeleteStorage deletes a storage from an application +func (s *ApplicationService) DeleteStorage(ctx context.Context, appUUID, storageUUID string) error { + err := s.client.Delete(ctx, fmt.Sprintf("applications/%s/storages/%s", appUUID, storageUUID)) + if err != nil { + return fmt.Errorf("failed to delete storage %s from application %s: %w", storageUUID, appUUID, err) + } + return nil +} + // CreatePublic creates an application from a public git repository func (s *ApplicationService) CreatePublic(ctx context.Context, req *models.ApplicationCreatePublicRequest) (*models.Application, error) { var app models.Application diff --git a/internal/service/application_test.go b/internal/service/application_test.go index e560197..853ba28 100644 --- a/internal/service/application_test.go +++ b/internal/service/application_test.go @@ -1265,3 +1265,264 @@ func TestApplicationService_BulkUpdateEnvs_APIError(t *testing.T) { assert.Nil(t, result) assert.Contains(t, err.Error(), "failed to bulk update environment variables") } + +func TestApplicationService_ListStorages(t *testing.T) { + hostPath := "/var/data" + content := "key: value" + resp := models.StoragesResponse{ + PersistentStorages: []models.PersistentStorage{ + { + ID: 1, + UUID: "ps-uuid-1", + Name: "data-volume", + MountPath: "/data", + HostPath: &hostPath, + IsPreviewSuffixEnabled: false, + IsReadOnly: false, + ResourceType: "App\\Models\\Application", + ResourceID: 10, + }, + }, + FileStorages: []models.FileStorage{ + { + ID: 2, + UUID: "fs-uuid-1", + FsPath: "/app/config.yml", + MountPath: "/app/config.yml", + Content: &content, + IsDirectory: false, + IsBasedOnGit: false, + IsPreviewSuffixEnabled: true, + ResourceType: "App\\Models\\Application", + ResourceID: 10, + }, + }, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/applications/app-uuid-123/storages", r.URL.Path) + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + result, err := svc.ListStorages(context.Background(), "app-uuid-123") + require.NoError(t, err) + assert.Len(t, result, 2) + assert.Equal(t, "ps-uuid-1", result[0].UUID) + assert.Equal(t, "persistent", result[0].Type) + assert.Equal(t, "data-volume", result[0].Name) + assert.Equal(t, "/data", result[0].MountPath) + assert.Equal(t, "/var/data", result[0].HostPath) + assert.Equal(t, false, result[0].IsPreviewSuffixEnabled) + assert.Equal(t, "fs-uuid-1", result[1].UUID) + assert.Equal(t, "file", result[1].Type) + assert.Equal(t, "/app/config.yml", result[1].Name) + assert.Equal(t, "key: value", result[1].Content) + assert.Equal(t, true, result[1].IsPreviewSuffixEnabled) +} + +func TestApplicationService_ListStorages_Empty(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/applications/app-uuid-123/storages", r.URL.Path) + assert.Equal(t, "GET", r.Method) + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"persistent_storages":[],"file_storages":[]}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + result, err := svc.ListStorages(context.Background(), "app-uuid-123") + require.NoError(t, err) + assert.Empty(t, result) +} + +func TestApplicationService_ListStorages_Error(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"application not found"}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + result, err := svc.ListStorages(context.Background(), "app-uuid-123") + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "failed to list storages") +} + +func TestApplicationService_UpdateStorage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/applications/app-uuid-123/storages", r.URL.Path) + assert.Equal(t, "PATCH", r.Method) + assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) + + var req models.StorageUpdateRequest + _ = json.NewDecoder(r.Body).Decode(&req) + assert.NotNil(t, req.UUID) + assert.Equal(t, "storage-uuid-1", *req.UUID) + assert.Equal(t, "persistent", req.Type) + assert.NotNil(t, req.Name) + assert.Equal(t, "new-name", *req.Name) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"message":"Storage updated."}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + name := "new-name" + storageUUID := "storage-uuid-1" + req := &models.StorageUpdateRequest{ + UUID: &storageUUID, + Type: "persistent", + Name: &name, + } + + err := svc.UpdateStorage(context.Background(), "app-uuid-123", req) + require.NoError(t, err) +} + +func TestApplicationService_UpdateStorage_NotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"application not found"}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + name := "new-name" + storageUUID := "storage-uuid-1" + req := &models.StorageUpdateRequest{ + UUID: &storageUUID, + Type: "persistent", + Name: &name, + } + + err := svc.UpdateStorage(context.Background(), "non-existent-uuid", req) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to update storage") +} + +func TestApplicationService_UpdateStorage_Error(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"message":"internal server error"}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + name := "new-name" + storageUUID := "storage-uuid-1" + req := &models.StorageUpdateRequest{ + UUID: &storageUUID, + Type: "persistent", + Name: &name, + } + + err := svc.UpdateStorage(context.Background(), "app-uuid-123", req) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to update storage") +} + +func TestApplicationService_CreateStorage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/applications/app-uuid-123/storages", r.URL.Path) + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) + + var req models.StorageCreateRequest + _ = json.NewDecoder(r.Body).Decode(&req) + assert.Equal(t, "persistent", req.Type) + assert.Equal(t, "/data", req.MountPath) + assert.NotNil(t, req.Name) + assert.Equal(t, "my-volume", *req.Name) + + w.WriteHeader(http.StatusCreated) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + name := "my-volume" + req := &models.StorageCreateRequest{ + Type: "persistent", + MountPath: "/data", + Name: &name, + } + + err := svc.CreateStorage(context.Background(), "app-uuid-123", req) + require.NoError(t, err) +} + +func TestApplicationService_CreateStorage_Error(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"message":"invalid request"}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + req := &models.StorageCreateRequest{ + Type: "persistent", + MountPath: "/data", + } + + err := svc.CreateStorage(context.Background(), "app-uuid-123", req) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to create storage") +} + +func TestApplicationService_DeleteStorage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/applications/app-uuid-123/storages/storage-uuid-1", r.URL.Path) + assert.Equal(t, "DELETE", r.Method) + assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization")) + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"message":"Storage deleted."}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + err := svc.DeleteStorage(context.Background(), "app-uuid-123", "storage-uuid-1") + require.NoError(t, err) +} + +func TestApplicationService_DeleteStorage_NotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(`{"message":"storage not found"}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + err := svc.DeleteStorage(context.Background(), "app-uuid-123", "nonexistent-uuid") + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to delete storage") +} diff --git a/internal/service/database.go b/internal/service/database.go index de799e5..7665c56 100644 --- a/internal/service/database.go +++ b/internal/service/database.go @@ -238,6 +238,43 @@ func (s *DatabaseService) BulkUpdateEnvs(ctx context.Context, dbUUID string, req return response, nil } +// ListStorages retrieves all storages for a database +func (s *DatabaseService) ListStorages(ctx context.Context, uuid string) ([]models.StorageListItem, error) { + var resp models.StoragesResponse + err := s.client.Get(ctx, fmt.Sprintf("databases/%s/storages", uuid), &resp) + if err != nil { + return nil, fmt.Errorf("failed to list storages for database %s: %w", uuid, err) + } + return models.MergeStorages(resp), nil +} + +// CreateStorage creates a new storage for a database +func (s *DatabaseService) CreateStorage(ctx context.Context, uuid string, req *models.StorageCreateRequest) error { + err := s.client.Post(ctx, fmt.Sprintf("databases/%s/storages", uuid), req, nil) + if err != nil { + return fmt.Errorf("failed to create storage for database %s: %w", uuid, err) + } + return nil +} + +// UpdateStorage updates a storage for a database +func (s *DatabaseService) UpdateStorage(ctx context.Context, uuid string, req *models.StorageUpdateRequest) error { + err := s.client.Patch(ctx, fmt.Sprintf("databases/%s/storages", uuid), req, nil) + if err != nil { + return fmt.Errorf("failed to update storage for database %s: %w", uuid, err) + } + return nil +} + +// DeleteStorage deletes a storage from a database +func (s *DatabaseService) DeleteStorage(ctx context.Context, dbUUID, storageUUID string) error { + err := s.client.Delete(ctx, fmt.Sprintf("databases/%s/storages/%s", dbUUID, storageUUID)) + if err != nil { + return fmt.Errorf("failed to delete storage %s from database %s: %w", storageUUID, dbUUID, err) + } + return nil +} + // inferDatabaseType determines the database type from available fields func inferDatabaseType(db *models.Database) string { // Check for PostgreSQL diff --git a/internal/service/database_test.go b/internal/service/database_test.go index 5c83608..d557595 100644 --- a/internal/service/database_test.go +++ b/internal/service/database_test.go @@ -1101,3 +1101,124 @@ func stringPtr(s string) *string { func boolPtr(b bool) *bool { return &b } + +func TestDatabaseService_ListStorages(t *testing.T) { + hostPath := "/var/data" + content := "key: value" + resp := models.StoragesResponse{ + PersistentStorages: []models.PersistentStorage{ + { + ID: 1, + UUID: "ps-uuid-1", + Name: "data-volume", + MountPath: "/data", + HostPath: &hostPath, + IsPreviewSuffixEnabled: true, + }, + }, + FileStorages: []models.FileStorage{ + { + ID: 2, + UUID: "fs-uuid-1", + FsPath: "/app/config.yml", + MountPath: "/app/config.yml", + Content: &content, + IsPreviewSuffixEnabled: false, + }, + }, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/databases/db-uuid-123/storages", r.URL.Path) + assert.Equal(t, "GET", r.Method) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewDatabaseService(client) + + result, err := svc.ListStorages(context.Background(), "db-uuid-123") + require.NoError(t, err) + assert.Len(t, result, 2) + assert.Equal(t, "persistent", result[0].Type) + assert.Equal(t, "data-volume", result[0].Name) + assert.Equal(t, true, result[0].IsPreviewSuffixEnabled) + assert.Equal(t, "file", result[1].Type) + assert.Equal(t, "/app/config.yml", result[1].Name) +} + +func TestDatabaseService_CreateStorage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/databases/db-uuid-123/storages", r.URL.Path) + assert.Equal(t, "POST", r.Method) + + var req models.StorageCreateRequest + _ = json.NewDecoder(r.Body).Decode(&req) + assert.Equal(t, "persistent", req.Type) + assert.Equal(t, "/data", req.MountPath) + + w.WriteHeader(http.StatusCreated) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewDatabaseService(client) + + name := "my-volume" + req := &models.StorageCreateRequest{ + Type: "persistent", + MountPath: "/data", + Name: &name, + } + + err := svc.CreateStorage(context.Background(), "db-uuid-123", req) + require.NoError(t, err) +} + +func TestDatabaseService_UpdateStorage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/databases/db-uuid-123/storages", r.URL.Path) + assert.Equal(t, "PATCH", r.Method) + + var req models.StorageUpdateRequest + _ = json.NewDecoder(r.Body).Decode(&req) + assert.Equal(t, "persistent", req.Type) + assert.NotNil(t, req.UUID) + assert.Equal(t, "storage-uuid-1", *req.UUID) + + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewDatabaseService(client) + + storageUUID := "storage-uuid-1" + name := "new-name" + req := &models.StorageUpdateRequest{ + UUID: &storageUUID, + Type: "persistent", + Name: &name, + } + + err := svc.UpdateStorage(context.Background(), "db-uuid-123", req) + require.NoError(t, err) +} + +func TestDatabaseService_DeleteStorage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/databases/db-uuid-123/storages/storage-uuid-1", r.URL.Path) + assert.Equal(t, "DELETE", r.Method) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"message":"Storage deleted."}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewDatabaseService(client) + + err := svc.DeleteStorage(context.Background(), "db-uuid-123", "storage-uuid-1") + require.NoError(t, err) +} diff --git a/internal/service/service.go b/internal/service/service.go index 5abd4b8..667c4ec 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -156,6 +156,43 @@ func (s *Service) DeleteEnv(ctx context.Context, serviceUUID, envUUID string) er return nil } +// ListStorages retrieves all storages for a service +func (s *Service) ListStorages(ctx context.Context, uuid string) ([]models.StorageListItem, error) { + var resp models.StoragesResponse + err := s.client.Get(ctx, fmt.Sprintf("services/%s/storages", uuid), &resp) + if err != nil { + return nil, fmt.Errorf("failed to list storages for service %s: %w", uuid, err) + } + return models.MergeStorages(resp), nil +} + +// CreateStorage creates a new storage for a service +func (s *Service) CreateStorage(ctx context.Context, uuid string, req *models.ServiceStorageCreateRequest) error { + err := s.client.Post(ctx, fmt.Sprintf("services/%s/storages", uuid), req, nil) + if err != nil { + return fmt.Errorf("failed to create storage for service %s: %w", uuid, err) + } + return nil +} + +// UpdateStorage updates a storage for a service +func (s *Service) UpdateStorage(ctx context.Context, uuid string, req *models.StorageUpdateRequest) error { + err := s.client.Patch(ctx, fmt.Sprintf("services/%s/storages", uuid), req, nil) + if err != nil { + return fmt.Errorf("failed to update storage for service %s: %w", uuid, err) + } + return nil +} + +// DeleteStorage deletes a storage from a service +func (s *Service) DeleteStorage(ctx context.Context, svcUUID, storageUUID string) error { + err := s.client.Delete(ctx, fmt.Sprintf("services/%s/storages/%s", svcUUID, storageUUID)) + if err != nil { + return fmt.Errorf("failed to delete storage %s from service %s: %w", storageUUID, svcUUID, err) + } + return nil +} + // BulkUpdateEnvs updates multiple environment variables in a single request func (s *Service) BulkUpdateEnvs(ctx context.Context, serviceUUID string, req *models.ServiceEnvBulkUpdateRequest) (models.ServiceEnvBulkUpdateResponse, error) { var response models.ServiceEnvBulkUpdateResponse diff --git a/internal/service/service_test.go b/internal/service/service_test.go index e2f1c98..7691908 100644 --- a/internal/service/service_test.go +++ b/internal/service/service_test.go @@ -2,6 +2,7 @@ package service import ( "context" + "encoding/json" "net/http" "net/http/httptest" "testing" @@ -439,3 +440,113 @@ func TestService_Create_Error(t *testing.T) { require.Error(t, err) } + +func TestService_ListStorages(t *testing.T) { + hostPath := "/var/data" + resp := models.StoragesResponse{ + PersistentStorages: []models.PersistentStorage{ + { + ID: 1, + UUID: "ps-uuid-1", + Name: "data-volume", + MountPath: "/data", + HostPath: &hostPath, + IsPreviewSuffixEnabled: true, + }, + }, + FileStorages: []models.FileStorage{}, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/services/svc-uuid-123/storages", r.URL.Path) + assert.Equal(t, "GET", r.Method) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewService(client) + + result, err := svc.ListStorages(context.Background(), "svc-uuid-123") + require.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, "persistent", result[0].Type) + assert.Equal(t, "data-volume", result[0].Name) + assert.Equal(t, true, result[0].IsPreviewSuffixEnabled) +} + +func TestService_CreateStorage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/services/svc-uuid-123/storages", r.URL.Path) + assert.Equal(t, "POST", r.Method) + + var req models.ServiceStorageCreateRequest + _ = json.NewDecoder(r.Body).Decode(&req) + assert.Equal(t, "persistent", req.Type) + assert.Equal(t, "/data", req.MountPath) + assert.Equal(t, "sub-resource-uuid", req.ResourceUUID) + + w.WriteHeader(http.StatusCreated) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewService(client) + + name := "my-volume" + req := &models.ServiceStorageCreateRequest{ + Type: "persistent", + MountPath: "/data", + ResourceUUID: "sub-resource-uuid", + Name: &name, + } + + err := svc.CreateStorage(context.Background(), "svc-uuid-123", req) + require.NoError(t, err) +} + +func TestService_UpdateStorage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/services/svc-uuid-123/storages", r.URL.Path) + assert.Equal(t, "PATCH", r.Method) + + var req models.StorageUpdateRequest + _ = json.NewDecoder(r.Body).Decode(&req) + assert.Equal(t, "persistent", req.Type) + assert.NotNil(t, req.UUID) + + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewService(client) + + storageUUID := "storage-uuid-1" + name := "new-name" + req := &models.StorageUpdateRequest{ + UUID: &storageUUID, + Type: "persistent", + Name: &name, + } + + err := svc.UpdateStorage(context.Background(), "svc-uuid-123", req) + require.NoError(t, err) +} + +func TestService_DeleteStorage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/services/svc-uuid-123/storages/storage-uuid-1", r.URL.Path) + assert.Equal(t, "DELETE", r.Method) + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"message":"Storage deleted."}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewService(client) + + err := svc.DeleteStorage(context.Background(), "svc-uuid-123", "storage-uuid-1") + require.NoError(t, err) +} From 53ab7b315cb3291eebd33a7363d3103eb78ddc5d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:38:19 +0100 Subject: [PATCH 2/3] feat(storage): require minimum API version 4.0.0-beta.470 Add version check validation across all storage CRUD operations in application, database, and service commands. This ensures the API client meets the minimum version requirement before executing storage operations. Also includes: - Documentation updates for dockerfile-target-build parameter in llms.txt - Field alignment formatting fixes in ApplicationCreateRequest structs --- cmd/application/storage/create.go | 4 + cmd/application/storage/delete.go | 4 + cmd/application/storage/list.go | 4 + cmd/application/storage/update.go | 4 + cmd/database/storage/create.go | 4 + cmd/database/storage/delete.go | 4 + cmd/database/storage/list.go | 4 + cmd/database/storage/update.go | 4 + cmd/service/storage/create.go | 4 + cmd/service/storage/delete.go | 4 + cmd/service/storage/list.go | 4 + cmd/service/storage/update.go | 4 + internal/models/application.go | 78 ++++----- llms.txt | 256 ++++++++++++++++++++++++++++++ 14 files changed, 343 insertions(+), 39 deletions(-) diff --git a/cmd/application/storage/create.go b/cmd/application/storage/create.go index 1579f7a..baee4a2 100644 --- a/cmd/application/storage/create.go +++ b/cmd/application/storage/create.go @@ -70,6 +70,10 @@ Examples: return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + appSvc := service.NewApplicationService(client) if err := appSvc.CreateStorage(ctx, args[0], req); err != nil { return fmt.Errorf("failed to create storage: %w", err) diff --git a/cmd/application/storage/delete.go b/cmd/application/storage/delete.go index 6a9c663..cbee2a5 100644 --- a/cmd/application/storage/delete.go +++ b/cmd/application/storage/delete.go @@ -27,6 +27,10 @@ Examples: return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + appSvc := service.NewApplicationService(client) if err := appSvc.DeleteStorage(ctx, args[0], args[1]); err != nil { return fmt.Errorf("failed to delete storage: %w", err) diff --git a/cmd/application/storage/list.go b/cmd/application/storage/list.go index 2a879a8..867d1bf 100644 --- a/cmd/application/storage/list.go +++ b/cmd/application/storage/list.go @@ -25,6 +25,10 @@ func NewListCommand() *cobra.Command { return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + appSvc := service.NewApplicationService(client) storages, err := appSvc.ListStorages(ctx, args[0]) if err != nil { diff --git a/cmd/application/storage/update.go b/cmd/application/storage/update.go index 29d1c57..4e6a4d5 100644 --- a/cmd/application/storage/update.go +++ b/cmd/application/storage/update.go @@ -90,6 +90,10 @@ Examples: return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + appSvc := service.NewApplicationService(client) if err := appSvc.UpdateStorage(ctx, args[0], req); err != nil { return fmt.Errorf("failed to update storage: %w", err) diff --git a/cmd/database/storage/create.go b/cmd/database/storage/create.go index ec0555d..cf0029a 100644 --- a/cmd/database/storage/create.go +++ b/cmd/database/storage/create.go @@ -68,6 +68,10 @@ Examples: return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + dbSvc := service.NewDatabaseService(client) if err := dbSvc.CreateStorage(ctx, args[0], req); err != nil { return fmt.Errorf("failed to create storage: %w", err) diff --git a/cmd/database/storage/delete.go b/cmd/database/storage/delete.go index eb4e93a..430b4e2 100644 --- a/cmd/database/storage/delete.go +++ b/cmd/database/storage/delete.go @@ -27,6 +27,10 @@ Examples: return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + dbSvc := service.NewDatabaseService(client) if err := dbSvc.DeleteStorage(ctx, args[0], args[1]); err != nil { return fmt.Errorf("failed to delete storage: %w", err) diff --git a/cmd/database/storage/list.go b/cmd/database/storage/list.go index 26981ed..38b7b00 100644 --- a/cmd/database/storage/list.go +++ b/cmd/database/storage/list.go @@ -25,6 +25,10 @@ func NewListCommand() *cobra.Command { return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + dbSvc := service.NewDatabaseService(client) storages, err := dbSvc.ListStorages(ctx, args[0]) if err != nil { diff --git a/cmd/database/storage/update.go b/cmd/database/storage/update.go index 3409f26..138272d 100644 --- a/cmd/database/storage/update.go +++ b/cmd/database/storage/update.go @@ -87,6 +87,10 @@ Examples: return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + dbSvc := service.NewDatabaseService(client) if err := dbSvc.UpdateStorage(ctx, args[0], req); err != nil { return fmt.Errorf("failed to update storage: %w", err) diff --git a/cmd/service/storage/create.go b/cmd/service/storage/create.go index 3e423f3..537bbb3 100644 --- a/cmd/service/storage/create.go +++ b/cmd/service/storage/create.go @@ -76,6 +76,10 @@ Examples: return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + svcSvc := service.NewService(client) if err := svcSvc.CreateStorage(ctx, args[0], req); err != nil { return fmt.Errorf("failed to create storage: %w", err) diff --git a/cmd/service/storage/delete.go b/cmd/service/storage/delete.go index 0206f95..51d867f 100644 --- a/cmd/service/storage/delete.go +++ b/cmd/service/storage/delete.go @@ -27,6 +27,10 @@ Examples: return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + svcSvc := service.NewService(client) if err := svcSvc.DeleteStorage(ctx, args[0], args[1]); err != nil { return fmt.Errorf("failed to delete storage: %w", err) diff --git a/cmd/service/storage/list.go b/cmd/service/storage/list.go index 8ba3313..40eae8e 100644 --- a/cmd/service/storage/list.go +++ b/cmd/service/storage/list.go @@ -25,6 +25,10 @@ func NewListCommand() *cobra.Command { return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + svcSvc := service.NewService(client) storages, err := svcSvc.ListStorages(ctx, args[0]) if err != nil { diff --git a/cmd/service/storage/update.go b/cmd/service/storage/update.go index 6f4c947..55578cd 100644 --- a/cmd/service/storage/update.go +++ b/cmd/service/storage/update.go @@ -87,6 +87,10 @@ Examples: return fmt.Errorf("failed to get API client: %w", err) } + if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.470"); err != nil { + return err + } + svcSvc := service.NewService(client) if err := svcSvc.UpdateStorage(ctx, args[0], req); err != nil { return fmt.Errorf("failed to update storage: %w", err) diff --git a/internal/models/application.go b/internal/models/application.go index d283d9f..493b805 100644 --- a/internal/models/application.go +++ b/internal/models/application.go @@ -390,9 +390,9 @@ type ApplicationCreatePublicRequest struct { BaseDirectory *string `json:"base_directory,omitempty"` PublishDirectory *string `json:"publish_directory,omitempty"` PortsMappings *string `json:"ports_mappings,omitempty"` - CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` - CustomLabels *string `json:"custom_labels,omitempty"` - DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` + CustomLabels *string `json:"custom_labels,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` // Health checks HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` @@ -434,15 +434,15 @@ type ApplicationCreateGitHubAppRequest struct { BaseDirectory *string `json:"base_directory,omitempty"` PublishDirectory *string `json:"publish_directory,omitempty"` PortsMappings *string `json:"ports_mappings,omitempty"` - CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` - CustomLabels *string `json:"custom_labels,omitempty"` - DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` - HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` - HealthCheckPath *string `json:"health_check_path,omitempty"` - HealthCheckPort *string `json:"health_check_port,omitempty"` - HealthCheckMethod *string `json:"health_check_method,omitempty"` - LimitsCPUs *string `json:"limits_cpus,omitempty"` - LimitsMemory *string `json:"limits_memory,omitempty"` + CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` + CustomLabels *string `json:"custom_labels,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` + HealthCheckPath *string `json:"health_check_path,omitempty"` + HealthCheckPort *string `json:"health_check_port,omitempty"` + HealthCheckMethod *string `json:"health_check_method,omitempty"` + LimitsCPUs *string `json:"limits_cpus,omitempty"` + LimitsMemory *string `json:"limits_memory,omitempty"` } // ApplicationCreateDeployKeyRequest for POST /applications/private-deploy-key @@ -474,15 +474,15 @@ type ApplicationCreateDeployKeyRequest struct { BaseDirectory *string `json:"base_directory,omitempty"` PublishDirectory *string `json:"publish_directory,omitempty"` PortsMappings *string `json:"ports_mappings,omitempty"` - CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` - CustomLabels *string `json:"custom_labels,omitempty"` - DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` - HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` - HealthCheckPath *string `json:"health_check_path,omitempty"` - HealthCheckPort *string `json:"health_check_port,omitempty"` - HealthCheckMethod *string `json:"health_check_method,omitempty"` - LimitsCPUs *string `json:"limits_cpus,omitempty"` - LimitsMemory *string `json:"limits_memory,omitempty"` + CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` + CustomLabels *string `json:"custom_labels,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` + HealthCheckPath *string `json:"health_check_path,omitempty"` + HealthCheckPort *string `json:"health_check_port,omitempty"` + HealthCheckMethod *string `json:"health_check_method,omitempty"` + LimitsCPUs *string `json:"limits_cpus,omitempty"` + LimitsMemory *string `json:"limits_memory,omitempty"` } // ApplicationCreateDockerfileRequest for POST /applications/dockerfile @@ -505,15 +505,15 @@ type ApplicationCreateDockerfileRequest struct { DestinationUUID *string `json:"destination_uuid,omitempty"` PortsExposes *string `json:"ports_exposes,omitempty"` PortsMappings *string `json:"ports_mappings,omitempty"` - CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` - CustomLabels *string `json:"custom_labels,omitempty"` - DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` - HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` - HealthCheckPath *string `json:"health_check_path,omitempty"` - HealthCheckPort *string `json:"health_check_port,omitempty"` - HealthCheckMethod *string `json:"health_check_method,omitempty"` - LimitsCPUs *string `json:"limits_cpus,omitempty"` - LimitsMemory *string `json:"limits_memory,omitempty"` + CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` + CustomLabels *string `json:"custom_labels,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` + HealthCheckPath *string `json:"health_check_path,omitempty"` + HealthCheckPort *string `json:"health_check_port,omitempty"` + HealthCheckMethod *string `json:"health_check_method,omitempty"` + LimitsCPUs *string `json:"limits_cpus,omitempty"` + LimitsMemory *string `json:"limits_memory,omitempty"` } // ApplicationCreateDockerImageRequest for POST /applications/dockerimage @@ -537,13 +537,13 @@ type ApplicationCreateDockerImageRequest struct { DestinationUUID *string `json:"destination_uuid,omitempty"` DockerRegistryImageTag *string `json:"docker_registry_image_tag,omitempty"` PortsMappings *string `json:"ports_mappings,omitempty"` - CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` - CustomLabels *string `json:"custom_labels,omitempty"` - DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` - HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` - HealthCheckPath *string `json:"health_check_path,omitempty"` - HealthCheckPort *string `json:"health_check_port,omitempty"` - HealthCheckMethod *string `json:"health_check_method,omitempty"` - LimitsCPUs *string `json:"limits_cpus,omitempty"` - LimitsMemory *string `json:"limits_memory,omitempty"` + CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"` + CustomLabels *string `json:"custom_labels,omitempty"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` + HealthCheckPath *string `json:"health_check_path,omitempty"` + HealthCheckPort *string `json:"health_check_port,omitempty"` + HealthCheckMethod *string `json:"health_check_method,omitempty"` + LimitsCPUs *string `json:"limits_cpus,omitempty"` + LimitsMemory *string `json:"limits_memory,omitempty"` } diff --git a/llms.txt b/llms.txt index 38972ad..7dd5520 100644 --- a/llms.txt +++ b/llms.txt @@ -45,6 +45,10 @@ Parameters: type: string description: Destination UUID if server has multiple destinations required: false + - name: --dockerfile-target-build + type: string + description: Dockerfile target build stage + required: false - name: --domains type: string description: Domain(s) for the application @@ -141,6 +145,10 @@ Parameters: type: string description: Dockerfile content (required) required: true + - name: --dockerfile-target-build + type: string + description: Dockerfile target build stage + required: false - name: --domains type: string description: Domain(s) for the application @@ -213,6 +221,10 @@ Parameters: type: string description: Docker image tag (defaults to 'latest') required: false + - name: --dockerfile-target-build + type: string + description: Dockerfile target build stage + required: false - name: --domains type: string description: Domain(s) for the application @@ -289,6 +301,10 @@ Parameters: type: string description: Destination UUID if server has multiple destinations required: false + - name: --dockerfile-target-build + type: string + description: Dockerfile target build stage + required: false - name: --domains type: string description: Domain(s) for the application @@ -393,6 +409,10 @@ Parameters: type: string description: Destination UUID if server has multiple destinations required: false + - name: --dockerfile-target-build + type: string + description: Dockerfile target build stage + required: false - name: --domains type: string description: Domain(s) for the application @@ -658,6 +678,82 @@ Command: coolify app stop Description: Stop a running application. Parameters: (None) +Command: coolify app storage create +Description: Create a storage for an application +Parameters: + - name: --content + type: string + description: File content (file only) + required: false + - name: --fs-path + type: string + description: Host directory path (file only, required when --is-directory is set) + required: false + - name: --host-path + type: string + description: Host path (persistent only) + required: false + - name: --is-directory + type: boolean + description: Whether this is a directory mount (file only) + required: false + - name: --mount-path + type: string + description: Mount path inside the container (required) + required: true + - name: --name + type: string + description: Volume name (persistent only) + required: false + - name: --type + type: string + description: Storage type: 'persistent' or 'file' (required) + required: true + +Command: coolify app storage delete +Description: Delete a storage from an application +Parameters: (None) + +Command: coolify app storage list +Description: List all persistent volumes and file storages for a specific application. +Parameters: (None) + +Command: coolify app storage update +Description: Update a storage for an application +Parameters: + - name: --content + type: string + description: File content (file only) + required: false + - name: --host-path + type: string + description: Host path (persistent only) + required: false + - name: --id + type: integer + description: Storage ID (deprecated, use --uuid instead) + required: false + - name: --is-preview-suffix-enabled + type: boolean + description: Enable preview suffix for this storage + required: false + - name: --mount-path + type: string + description: Mount path inside the container + required: false + - name: --name + type: string + description: Storage name (persistent only) + required: false + - name: --type + type: string + description: Storage type: 'persistent' or 'file' (required) + required: true + - name: --uuid + type: string + description: Storage UUID (required, use 'storage list' to find) + required: false + Command: coolify app update Description: Update configuration for a specific application. Only specified fields will be updated. Parameters: @@ -685,6 +781,10 @@ Parameters: type: string description: Dockerfile content required: false + - name: --dockerfile-target-build + type: string + description: Dockerfile target build stage + required: false - name: --domains type: string description: Domains (comma-separated) @@ -1194,6 +1294,82 @@ Command: coolify database stop Description: Stop a database by UUID. Parameters: (None) +Command: coolify database storage create +Description: Create a storage for a database +Parameters: + - name: --content + type: string + description: File content (file only) + required: false + - name: --fs-path + type: string + description: Host directory path (file only, required when --is-directory is set) + required: false + - name: --host-path + type: string + description: Host path (persistent only) + required: false + - name: --is-directory + type: boolean + description: Whether this is a directory mount (file only) + required: false + - name: --mount-path + type: string + description: Mount path inside the container (required) + required: true + - name: --name + type: string + description: Volume name (persistent only) + required: false + - name: --type + type: string + description: Storage type: 'persistent' or 'file' (required) + required: true + +Command: coolify database storage delete +Description: Delete a storage from a database +Parameters: (None) + +Command: coolify database storage list +Description: List all persistent volumes and file storages for a specific database. +Parameters: (None) + +Command: coolify database storage update +Description: Update a storage for a database +Parameters: + - name: --content + type: string + description: File content (file only) + required: false + - name: --host-path + type: string + description: Host path (persistent only) + required: false + - name: --id + type: integer + description: Storage ID (deprecated, use --uuid instead) + required: false + - name: --is-preview-suffix-enabled + type: boolean + description: Enable preview suffix for this storage + required: false + - name: --mount-path + type: string + description: Mount path inside the container + required: false + - name: --name + type: string + description: Storage name (persistent only) + required: false + - name: --type + type: string + description: Storage type: 'persistent' or 'file' (required) + required: true + - name: --uuid + type: string + description: Storage UUID (required, use 'storage list' to find) + required: false + Command: coolify database update Description: Update a database's configuration by UUID. Parameters: @@ -1666,6 +1842,86 @@ Command: coolify service stop Description: Stop a service (stop all containers). Parameters: (None) +Command: coolify service storage create +Description: Create a storage for a service +Parameters: + - name: --content + type: string + description: File content (file only) + required: false + - name: --fs-path + type: string + description: Host directory path (file only, required when --is-directory is set) + required: false + - name: --host-path + type: string + description: Host path (persistent only) + required: false + - name: --is-directory + type: boolean + description: Whether this is a directory mount (file only) + required: false + - name: --mount-path + type: string + description: Mount path inside the container (required) + required: true + - name: --name + type: string + description: Volume name (persistent only) + required: false + - name: --resource-uuid + type: string + description: UUID of the service sub-resource (required) + required: true + - name: --type + type: string + description: Storage type: 'persistent' or 'file' (required) + required: true + +Command: coolify service storage delete +Description: Delete a storage from a service +Parameters: (None) + +Command: coolify service storage list +Description: List all persistent volumes and file storages for a specific service. +Parameters: (None) + +Command: coolify service storage update +Description: Update a storage for a service +Parameters: + - name: --content + type: string + description: File content (file only) + required: false + - name: --host-path + type: string + description: Host path (persistent only) + required: false + - name: --id + type: integer + description: Storage ID (deprecated, use --uuid instead) + required: false + - name: --is-preview-suffix-enabled + type: boolean + description: Enable preview suffix for this storage + required: false + - name: --mount-path + type: string + description: Mount path inside the container + required: false + - name: --name + type: string + description: Storage name (persistent only) + required: false + - name: --type + type: string + description: Storage type: 'persistent' or 'file' (required) + required: true + - name: --uuid + type: string + description: Storage UUID (required, use 'storage list' to find) + required: false + Command: coolify teams current Description: Get details of the team associated with the current authentication token. Parameters: (None) From cad379eefbdd51bc1abf28910d4efd3ead196b2f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:40:55 +0100 Subject: [PATCH 3/3] test(service): use assert.True/False helpers for boolean assertions Replace assert.Equal with more specific boolean assertion helpers (assert.True and assert.False) for improved readability and idiomatic Go testing practices. --- internal/service/application_test.go | 4 ++-- internal/service/database_test.go | 2 +- internal/service/service_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/service/application_test.go b/internal/service/application_test.go index f0bcb62..f2710f3 100644 --- a/internal/service/application_test.go +++ b/internal/service/application_test.go @@ -1356,12 +1356,12 @@ func TestApplicationService_ListStorages(t *testing.T) { assert.Equal(t, "data-volume", result[0].Name) assert.Equal(t, "/data", result[0].MountPath) assert.Equal(t, "/var/data", result[0].HostPath) - assert.Equal(t, false, result[0].IsPreviewSuffixEnabled) + assert.False(t, result[0].IsPreviewSuffixEnabled) assert.Equal(t, "fs-uuid-1", result[1].UUID) assert.Equal(t, "file", result[1].Type) assert.Equal(t, "/app/config.yml", result[1].Name) assert.Equal(t, "key: value", result[1].Content) - assert.Equal(t, true, result[1].IsPreviewSuffixEnabled) + assert.True(t, result[1].IsPreviewSuffixEnabled) } func TestApplicationService_ListStorages_Empty(t *testing.T) { diff --git a/internal/service/database_test.go b/internal/service/database_test.go index d557595..3c45c24 100644 --- a/internal/service/database_test.go +++ b/internal/service/database_test.go @@ -1144,7 +1144,7 @@ func TestDatabaseService_ListStorages(t *testing.T) { assert.Len(t, result, 2) assert.Equal(t, "persistent", result[0].Type) assert.Equal(t, "data-volume", result[0].Name) - assert.Equal(t, true, result[0].IsPreviewSuffixEnabled) + assert.True(t, result[0].IsPreviewSuffixEnabled) assert.Equal(t, "file", result[1].Type) assert.Equal(t, "/app/config.yml", result[1].Name) } diff --git a/internal/service/service_test.go b/internal/service/service_test.go index 7691908..31c5bc0 100644 --- a/internal/service/service_test.go +++ b/internal/service/service_test.go @@ -473,7 +473,7 @@ func TestService_ListStorages(t *testing.T) { assert.Len(t, result, 1) assert.Equal(t, "persistent", result[0].Type) assert.Equal(t, "data-volume", result[0].Name) - assert.Equal(t, true, result[0].IsPreviewSuffixEnabled) + assert.True(t, result[0].IsPreviewSuffixEnabled) } func TestService_CreateStorage(t *testing.T) {