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..baee4a2 --- /dev/null +++ b/cmd/application/storage/create.go @@ -0,0 +1,96 @@ +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) + } + + 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) + } + + 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..cbee2a5 --- /dev/null +++ b/cmd/application/storage/delete.go @@ -0,0 +1,43 @@ +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) + } + + 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) + } + + 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..867d1bf --- /dev/null +++ b/cmd/application/storage/list.go @@ -0,0 +1,51 @@ +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) + } + + 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 { + 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..4e6a4d5 --- /dev/null +++ b/cmd/application/storage/update.go @@ -0,0 +1,117 @@ +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) + } + + 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) + } + + 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..cf0029a --- /dev/null +++ b/cmd/database/storage/create.go @@ -0,0 +1,94 @@ +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) + } + + 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) + } + + 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..430b4e2 --- /dev/null +++ b/cmd/database/storage/delete.go @@ -0,0 +1,43 @@ +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) + } + + 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) + } + + 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..38b7b00 --- /dev/null +++ b/cmd/database/storage/list.go @@ -0,0 +1,51 @@ +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) + } + + 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 { + 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..138272d --- /dev/null +++ b/cmd/database/storage/update.go @@ -0,0 +1,114 @@ +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) + } + + 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) + } + + 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..537bbb3 --- /dev/null +++ b/cmd/service/storage/create.go @@ -0,0 +1,103 @@ +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) + } + + 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) + } + + 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..51d867f --- /dev/null +++ b/cmd/service/storage/delete.go @@ -0,0 +1,43 @@ +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) + } + + 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) + } + + 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..40eae8e --- /dev/null +++ b/cmd/service/storage/list.go @@ -0,0 +1,51 @@ +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) + } + + 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 { + 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..55578cd --- /dev/null +++ b/cmd/service/storage/update.go @@ -0,0 +1,114 @@ +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) + } + + 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) + } + + 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 a4303b6..493b805 100644 --- a/internal/models/application.go +++ b/internal/models/application.go @@ -143,6 +143,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"` @@ -237,6 +238,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 { @@ -267,6 +392,7 @@ type ApplicationCreatePublicRequest struct { 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"` // Health checks HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` @@ -310,6 +436,7 @@ type ApplicationCreateGitHubAppRequest struct { 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"` @@ -349,6 +476,7 @@ type ApplicationCreateDeployKeyRequest struct { 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"` @@ -379,6 +507,7 @@ type ApplicationCreateDockerfileRequest struct { 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"` @@ -410,6 +539,7 @@ type ApplicationCreateDockerImageRequest struct { 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"` 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 2584aab..f2710f3 100644 --- a/internal/service/application_test.go +++ b/internal/service/application_test.go @@ -1301,3 +1301,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.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.True(t, 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..3c45c24 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.True(t, 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..31c5bc0 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.True(t, 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) +} 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)