mirror of
https://github.com/coollabsio/coolify-cli.git
synced 2026-06-19 15:45:03 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c45a0b35ec | |||
| 5e2b3d08db | |||
| b0eb8dbd15 | |||
| c292ba8b42 | |||
| 4ae6065ecf | |||
| 80bc511fd8 | |||
| b2da3013d2 | |||
| 28d54b0df9 | |||
| c6378a8280 | |||
| ce0e8fe9cd | |||
| 528b1359aa | |||
| eabce9a8e1 | |||
| f43cd16f6f | |||
| e49daeea95 | |||
| ccf578e537 | |||
| cad379eefb | |||
| 53ab7b315c | |||
| 8a7d2c20af | |||
| fcd1a01fb7 | |||
| f67411de2c | |||
| 146ce7a7b0 | |||
| b661576fc1 | |||
| 0872e48283 | |||
| 303fad333b | |||
| 8ee7ec4c0d | |||
| 98f40f03dc | |||
| 28521a2ca0 | |||
| dd4b271faf | |||
| cdc5a1e732 | |||
| 7e3639b41a | |||
| 6bd783dc8a | |||
| 2ac1d0f869 | |||
| f4c4c962ff | |||
| 801c2e0b3c | |||
| ea4bec7492 | |||
| 7e59cd76c3 | |||
| 81b9e9cdd0 | |||
| fe01e8f9b8 | |||
| daa2a4cdcb | |||
| 1703fd2e52 | |||
| 333ff3c504 | |||
| 0daae657fb | |||
| ea3236672b |
@@ -28,6 +28,7 @@ jobs:
|
||||
workdir: ./
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||
|
||||
update-version:
|
||||
needs: [release-cli]
|
||||
|
||||
@@ -34,6 +34,26 @@ jobs:
|
||||
- name: Run tests
|
||||
run: go test -v -race -cover ./...
|
||||
|
||||
llms-txt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Regenerate llms.txt
|
||||
run: go run ./coolify docs llms
|
||||
|
||||
- name: Check uncommitted changes
|
||||
run: git diff --exit-code llms.txt llms-full.txt
|
||||
|
||||
- if: failure()
|
||||
run: echo "::error::llms.txt or llms-full.txt is out of date. Run 'go run ./coolify docs llms' and commit the changes."
|
||||
|
||||
go-mod-tidy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -52,4 +72,4 @@ jobs:
|
||||
run: git diff --exit-code
|
||||
|
||||
- if: failure()
|
||||
run: echo "::error::Check failed, please run 'go mod tidy' and commit the changes."
|
||||
run: echo "::error::Check failed, please run 'go mod tidy' and commit the changes."
|
||||
|
||||
+16
-1
@@ -36,4 +36,19 @@ archives:
|
||||
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
formats: [zip]
|
||||
|
||||
brews:
|
||||
- name: coolify-cli
|
||||
repository:
|
||||
owner: coollabsio
|
||||
name: homebrew-coolify-cli
|
||||
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
|
||||
directory: Formula
|
||||
homepage: "https://coolify.io"
|
||||
description: "CLI tool for interacting with the Coolify API"
|
||||
license: "MIT"
|
||||
install: |
|
||||
bin.install "coolify"
|
||||
test: |
|
||||
system "#{bin}/coolify", "version"
|
||||
@@ -12,6 +12,12 @@ curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts
|
||||
|
||||
It will install the CLI in `/usr/local/bin/coolify` and the configuration file in `~/.config/coolify/config.json`
|
||||
|
||||
### Homebrew (macOS/Linux)
|
||||
|
||||
```bash
|
||||
brew install coollabsio/coolify-cli/coolify-cli
|
||||
```
|
||||
|
||||
#### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
@@ -58,6 +64,16 @@ This will install the `coolify` binary in your `$GOPATH/bin` directory (usually
|
||||
|
||||
Now you can use the CLI with the token you just added.
|
||||
|
||||
## For LLMs / AI agents
|
||||
|
||||
- Quick instructions: [`llms.txt`](./llms.txt)
|
||||
- Full command catalog: [`llms-full.txt`](./llms-full.txt)
|
||||
- Regenerate both files:
|
||||
|
||||
```bash
|
||||
go run ./coolify docs llms
|
||||
```
|
||||
|
||||
## Change default context
|
||||
You can change the default context with `coolify context use <context_name>` or `coolify context set-default <context_name>`
|
||||
## Currently Supported Commands
|
||||
@@ -149,9 +165,9 @@ Commands can use `server` or `servers` interchangeably.
|
||||
- `--build-time` - Available at build time
|
||||
- `--is-literal` - Treat value as literal (don't interpolate variables)
|
||||
- `--is-multiline` - Value is multiline
|
||||
- `coolify app env update <app_uuid>` - Update an environment variable
|
||||
- `--key <key>` - Variable key (required)
|
||||
- `coolify app env update <app_uuid> <env_uuid_or_key>` - Update an environment variable
|
||||
- `--value <value>` - Variable value (required)
|
||||
- `--key <key>` - New variable key (optional, for renaming)
|
||||
- `--preview` - Available in preview deployments
|
||||
- `--build-time` - Available at build time
|
||||
- `--is-literal` - Treat value as literal (don't interpolate variables)
|
||||
@@ -239,9 +255,9 @@ Commands can use `server` or `servers` interchangeably.
|
||||
- `coolify service env get <service_uuid> <env_uuid_or_key>` - Get a specific environment variable
|
||||
- `coolify service env create <service_uuid>` - Create a new environment variable
|
||||
- Same flags as application environment variables
|
||||
- `coolify service env update <service_uuid>` - Update an environment variable
|
||||
- `--key <key>` - Variable key (required)
|
||||
- `coolify service env update <service_uuid> <env_uuid_or_key>` - Update an environment variable
|
||||
- `--value <value>` - Variable value (required)
|
||||
- `--key <key>` - New variable key (optional, for renaming)
|
||||
- `--build-time` - Available at build time
|
||||
- `--is-literal` - Treat value as literal (don't interpolate variables)
|
||||
- `--is-multiline` - Value is multiline
|
||||
@@ -257,10 +273,16 @@ Commands can use `server` or `servers` interchangeably.
|
||||
### Deployments
|
||||
- `coolify deploy uuid <uuid>` - Deploy a resource by UUID
|
||||
- `-f, --force` - Force deployment
|
||||
- `--pull-request-id <id>` - Pull request ID for preview deployments
|
||||
- `--docker-tag <tag>` - Docker image tag override for the deployment (requires Coolify `4.0.0-beta.471+`)
|
||||
- `coolify deploy name <name>` - Deploy a resource by name
|
||||
- `-f, --force` - Force deployment
|
||||
- `--pull-request-id <id>` - Pull request ID for preview deployments
|
||||
- `--docker-tag <tag>` - Docker image tag override for the deployment (requires Coolify `4.0.0-beta.471+`)
|
||||
- `coolify deploy batch <name1,name2,...>` - Deploy multiple resources at once
|
||||
- `-f, --force` - Force all deployments
|
||||
- `--pull-request-id <id>` - Pull request ID for preview deployments
|
||||
- `--docker-tag <tag>` - Docker image tag override for the deployment (requires Coolify `4.0.0-beta.471+`)
|
||||
- `coolify deploy list` - List all deployments
|
||||
- `coolify deploy get <uuid>` - Get deployment details
|
||||
- `coolify deploy cancel <uuid>` - Cancel a deployment
|
||||
@@ -421,6 +443,9 @@ coolify deploy batch api,worker,frontend
|
||||
# Force deploy with specific context
|
||||
coolify --context=prod deploy batch api,worker --force
|
||||
|
||||
# Deploy a preview with an explicit docker tag
|
||||
coolify deploy uuid u5ualfp30j27qtfpgcen8p03 --pull-request-id 2345 --docker-tag 1.28.3
|
||||
|
||||
# Traditional UUID deployment still works
|
||||
coolify deploy uuid abc123-def456-...
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Vendored
+6
-1
@@ -60,6 +60,10 @@ func NewCreateEnvCommand() *cobra.Command {
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
if cmd.Flags().Changed("comment") {
|
||||
comment, _ := cmd.Flags().GetString("comment")
|
||||
req.Comment = &comment
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
env, err := appSvc.CreateEnv(ctx, appUUID, req)
|
||||
@@ -67,7 +71,7 @@ func NewCreateEnvCommand() *cobra.Command {
|
||||
return fmt.Errorf("failed to create environment variable: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Environment variable '%s' created successfully.\n", env.Key)
|
||||
fmt.Printf("Environment variable '%s' created successfully.\n", key)
|
||||
fmt.Printf("UUID: %s\n", env.UUID)
|
||||
return nil
|
||||
},
|
||||
@@ -80,5 +84,6 @@ func NewCreateEnvCommand() *cobra.Command {
|
||||
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
|
||||
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
|
||||
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
|
||||
cmd.Flags().String("comment", "", "Comment for the environment variable")
|
||||
return cmd
|
||||
}
|
||||
|
||||
Vendored
+23
-9
@@ -12,13 +12,14 @@ import (
|
||||
|
||||
func NewUpdateEnvCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update <app_uuid>",
|
||||
Use: "update <app_uuid> <env_uuid_or_key>",
|
||||
Short: "Update an environment variable",
|
||||
Long: `Update an existing environment variable. UUID is the application.`,
|
||||
Args: cli.ExactArgs(1, "<app_uuid>"),
|
||||
Long: `Update an existing environment variable. Identify it by UUID or key name.`,
|
||||
Args: cli.ExactArgs(2, "<app_uuid> <env_uuid_or_key>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
envIdentifier := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
@@ -30,12 +31,24 @@ func NewUpdateEnvCommand() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
|
||||
// Look up the env var to resolve its key
|
||||
existingEnv, err := appSvc.GetEnv(ctx, appUUID, envIdentifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find environment variable '%s': %w", envIdentifier, err)
|
||||
}
|
||||
|
||||
req := &models.EnvironmentVariableUpdateRequest{}
|
||||
|
||||
// Use existing key unless --key flag explicitly provides a new one
|
||||
if cmd.Flags().Changed("key") {
|
||||
key, _ := cmd.Flags().GetString("key")
|
||||
req.Key = &key
|
||||
} else {
|
||||
req.Key = &existingEnv.Key
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("value") {
|
||||
value, _ := cmd.Flags().GetString("value")
|
||||
req.Value = &value
|
||||
@@ -60,15 +73,15 @@ func NewUpdateEnvCommand() *cobra.Command {
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
if req.Key == nil {
|
||||
return fmt.Errorf("--key is required")
|
||||
if cmd.Flags().Changed("comment") {
|
||||
comment, _ := cmd.Flags().GetString("comment")
|
||||
req.Comment = &comment
|
||||
}
|
||||
|
||||
if req.Value == nil {
|
||||
return fmt.Errorf("--value is required")
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
env, err := appSvc.UpdateEnv(ctx, appUUID, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update environment variable: %w", err)
|
||||
@@ -79,12 +92,13 @@ func NewUpdateEnvCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("key", "", "New environment variable key")
|
||||
cmd.Flags().String("value", "", "New environment variable value")
|
||||
cmd.Flags().String("key", "", "New environment variable key (rename)")
|
||||
cmd.Flags().String("value", "", "New environment variable value (required)")
|
||||
cmd.Flags().Bool("build-time", true, "Available at build time (default: true)")
|
||||
cmd.Flags().Bool("preview", false, "Available in preview deployments")
|
||||
cmd.Flags().Bool("is-literal", false, "Treat value as literal")
|
||||
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
|
||||
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
|
||||
cmd.Flags().String("comment", "", "Comment for the environment variable")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -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 <app_uuid>",
|
||||
Short: "Create a storage for an application",
|
||||
Long: `Create a persistent volume or file storage for an application.
|
||||
|
||||
Examples:
|
||||
coolify app storage create <app_uuid> --type persistent --name my-volume --mount-path /data
|
||||
coolify app storage create <app_uuid> --type persistent --name my-volume --mount-path /data --host-path /var/data
|
||||
coolify app storage create <app_uuid> --type file --mount-path /app/config.yml --content "key: value"
|
||||
coolify app storage create <app_uuid> --type file --mount-path /app/data --is-directory --fs-path /app/data`,
|
||||
Args: cli.ExactArgs(1, "<app_uuid>"),
|
||||
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
|
||||
}
|
||||
@@ -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 <app_uuid> <storage_uuid>",
|
||||
Short: "Delete a storage from an application",
|
||||
Long: `Delete a persistent volume or file storage from an application.
|
||||
|
||||
Examples:
|
||||
coolify app storage delete <app_uuid> <storage_uuid>`,
|
||||
Args: cli.ExactArgs(2, "<app_uuid> <storage_uuid>"),
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 <app_uuid>",
|
||||
Short: "List all storages for an application",
|
||||
Long: `List all persistent volumes and file storages for a specific application.`,
|
||||
Args: cli.ExactArgs(1, "<app_uuid>"),
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 <app_uuid>",
|
||||
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 <app_uuid> --uuid <storage_uuid> --type persistent --name my-volume --mount-path /data
|
||||
coolify app storage update <app_uuid> --uuid <storage_uuid> --type file --content "config content" --mount-path /app/config.yml
|
||||
coolify app storage update <app_uuid> --uuid <storage_uuid> --type persistent --is-preview-suffix-enabled`,
|
||||
Args: cli.ExactArgs(1, "<app_uuid>"),
|
||||
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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"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
|
||||
@@ -25,6 +27,19 @@ func NewDatabaseCommand() *cobra.Command {
|
||||
cmd.AddCommand(NewUpdateCommand())
|
||||
cmd.AddCommand(NewDeleteCommand())
|
||||
|
||||
// Add env subcommand
|
||||
envCmd := &cobra.Command{
|
||||
Use: "env",
|
||||
Short: "Manage database environment variables",
|
||||
}
|
||||
envCmd.AddCommand(env.NewListCommand())
|
||||
envCmd.AddCommand(env.NewGetCommand())
|
||||
envCmd.AddCommand(env.NewCreateCommand())
|
||||
envCmd.AddCommand(env.NewUpdateCommand())
|
||||
envCmd.AddCommand(env.NewDeleteCommand())
|
||||
envCmd.AddCommand(env.NewSyncCommand())
|
||||
cmd.AddCommand(envCmd)
|
||||
|
||||
// Add backup subcommand
|
||||
backupCmd := &cobra.Command{
|
||||
Use: "backup",
|
||||
@@ -39,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
|
||||
}
|
||||
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
package env
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create <database_uuid>",
|
||||
Short: "Create an environment variable for a database",
|
||||
Long: `Create a new environment variable for a specific database. Use --key and --value flags to specify the variable.`,
|
||||
Args: cli.ExactArgs(1, "<database_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
key, _ := cmd.Flags().GetString("key")
|
||||
value, _ := cmd.Flags().GetString("value")
|
||||
|
||||
if key == "" {
|
||||
return fmt.Errorf("--key is required")
|
||||
}
|
||||
if value == "" {
|
||||
return fmt.Errorf("--value is required")
|
||||
}
|
||||
|
||||
req := &models.DatabaseEnvironmentVariableCreateRequest{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("is-literal") {
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
req.IsLiteral = &isLiteral
|
||||
}
|
||||
if cmd.Flags().Changed("is-multiline") {
|
||||
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
|
||||
req.IsMultiline = &isMultiline
|
||||
}
|
||||
if cmd.Flags().Changed("is-shown-once") {
|
||||
isShownOnce, _ := cmd.Flags().GetBool("is-shown-once")
|
||||
req.IsShownOnce = &isShownOnce
|
||||
}
|
||||
if cmd.Flags().Changed("comment") {
|
||||
comment, _ := cmd.Flags().GetString("comment")
|
||||
req.Comment = &comment
|
||||
}
|
||||
|
||||
dbSvc := service.NewDatabaseService(client)
|
||||
_, err = dbSvc.CreateEnv(ctx, uuid, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create environment variable: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Environment variable '%s' created successfully.\n", key)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("key", "", "Environment variable key (required)")
|
||||
cmd.Flags().String("value", "", "Environment variable value (required)")
|
||||
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
|
||||
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
|
||||
cmd.Flags().Bool("is-shown-once", false, "Only show value once")
|
||||
cmd.Flags().String("comment", "", "Comment for the environment variable")
|
||||
|
||||
return cmd
|
||||
}
|
||||
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewDeleteCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete <database_uuid> <env_uuid>",
|
||||
Short: "Delete an environment variable",
|
||||
Long: `Delete an environment variable from a database. First UUID is the database, second is the specific environment variable to delete.`,
|
||||
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbUUID := args[0]
|
||||
envUUID := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
// Prompt for confirmation unless --force is used
|
||||
if !force {
|
||||
var response string
|
||||
fmt.Printf("Are you sure you want to delete this environment variable? (yes/no): ")
|
||||
_, _ = fmt.Scanln(&response)
|
||||
|
||||
if response != "yes" && response != "y" {
|
||||
fmt.Println("Delete cancelled.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
dbSvc := service.NewDatabaseService(client)
|
||||
err = dbSvc.DeleteEnv(ctx, dbUUID, envUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete environment variable: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Environment variable deleted successfully.")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("force", false, "Skip confirmation prompt")
|
||||
|
||||
return cmd
|
||||
}
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
package env
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func NewGetCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "get <database_uuid> <env_uuid_or_key>",
|
||||
Short: "Get environment variable details",
|
||||
Long: `Get detailed information about a specific environment variable. First UUID is the database, second is the environment variable UUID or key name.`,
|
||||
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbUUID := args[0]
|
||||
envUUID := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbSvc := service.NewDatabaseService(client)
|
||||
env, err := dbSvc.GetEnv(ctx, dbUUID, envUUID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get environment variable: %w", err)
|
||||
}
|
||||
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
// Mask sensitive value unless --show-sensitive is used
|
||||
if !showSensitive {
|
||||
env.Value = "********"
|
||||
if env.RealValue != nil {
|
||||
masked := "********"
|
||||
env.RealValue = &masked
|
||||
}
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(env)
|
||||
},
|
||||
}
|
||||
}
|
||||
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
package env
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func NewListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list <database_uuid>",
|
||||
Short: "List all environment variables for a database",
|
||||
Long: `List all environment variables for a specific database.`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
dbSvc := service.NewDatabaseService(client)
|
||||
envs, err := dbSvc.ListEnvs(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list environment variables: %w", err)
|
||||
}
|
||||
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
// Mask sensitive values unless --show-sensitive is used
|
||||
if !showSensitive {
|
||||
for i := range envs {
|
||||
envs[i].Value = "********"
|
||||
if envs[i].RealValue != nil {
|
||||
masked := "********"
|
||||
envs[i].RealValue = &masked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(envs)
|
||||
},
|
||||
}
|
||||
}
|
||||
Vendored
+145
@@ -0,0 +1,145 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/parser"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewSyncCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "sync <database_uuid>",
|
||||
Short: "Sync environment variables from a .env file",
|
||||
Long: `Sync environment variables from a .env file. This command intelligently:
|
||||
- Updates existing environment variables with new values
|
||||
- Creates new environment variables that don't exist yet
|
||||
- Uses efficient bulk operations where possible
|
||||
|
||||
Example: coolify db env sync abc123 --file .env.production`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
uuid := args[0]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
filePath, _ := cmd.Flags().GetString("file")
|
||||
if filePath == "" {
|
||||
return fmt.Errorf("--file is required")
|
||||
}
|
||||
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
|
||||
// Parse the .env file
|
||||
envVars, err := parser.ParseEnvFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse .env file: %w", err)
|
||||
}
|
||||
|
||||
if len(envVars) == 0 {
|
||||
fmt.Println("No environment variables found in file.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d environment variables in file. Syncing...\n", len(envVars))
|
||||
|
||||
// Fetch existing environment variables
|
||||
dbSvc := service.NewDatabaseService(client)
|
||||
existingEnvs, err := dbSvc.ListEnvs(ctx, uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list existing environment variables: %w", err)
|
||||
}
|
||||
|
||||
// Build a map of existing env vars by key
|
||||
existingMap := make(map[string]models.DatabaseEnvironmentVariable)
|
||||
for _, env := range existingEnvs {
|
||||
existingMap[env.Key] = env
|
||||
}
|
||||
|
||||
// Separate into updates and creates
|
||||
var toUpdate []models.DatabaseEnvironmentVariableCreateRequest
|
||||
var toCreate []models.DatabaseEnvironmentVariableCreateRequest
|
||||
|
||||
for _, envVar := range envVars {
|
||||
req := models.DatabaseEnvironmentVariableCreateRequest{
|
||||
Key: envVar.Key,
|
||||
Value: envVar.Value,
|
||||
}
|
||||
|
||||
// Apply flags if explicitly provided
|
||||
if cmd.Flags().Changed("is-literal") {
|
||||
req.IsLiteral = &isLiteral
|
||||
}
|
||||
|
||||
// Auto-detect multiline values
|
||||
if strings.Contains(envVar.Value, "\n") {
|
||||
multiline := true
|
||||
req.IsMultiline = &multiline
|
||||
}
|
||||
|
||||
if _, exists := existingMap[envVar.Key]; exists {
|
||||
toUpdate = append(toUpdate, req)
|
||||
} else {
|
||||
toCreate = append(toCreate, req)
|
||||
}
|
||||
}
|
||||
|
||||
updateCount := 0
|
||||
createCount := 0
|
||||
failCount := 0
|
||||
|
||||
// Perform bulk update if there are vars to update
|
||||
if len(toUpdate) > 0 {
|
||||
fmt.Printf("Updating %d existing variables...\n", len(toUpdate))
|
||||
bulkReq := &models.DatabaseEnvBulkUpdateRequest{
|
||||
Data: toUpdate,
|
||||
}
|
||||
_, err := dbSvc.BulkUpdateEnvs(ctx, uuid, bulkReq)
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ Bulk update failed: %v\n", err)
|
||||
failCount += len(toUpdate)
|
||||
} else {
|
||||
updateCount = len(toUpdate)
|
||||
fmt.Printf(" ✓ Successfully updated %d variables\n", updateCount)
|
||||
}
|
||||
}
|
||||
|
||||
// Create new variables one by one
|
||||
if len(toCreate) > 0 {
|
||||
fmt.Printf("Creating %d new variables...\n", len(toCreate))
|
||||
for _, req := range toCreate {
|
||||
_, err := dbSvc.CreateEnv(ctx, uuid, &req)
|
||||
if err != nil {
|
||||
fmt.Printf(" ✗ Failed to create '%s': %v\n", req.Key, err)
|
||||
failCount++
|
||||
} else {
|
||||
fmt.Printf(" ✓ Created '%s'\n", req.Key)
|
||||
createCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nSync complete: %d updated, %d created, %d failed\n", updateCount, createCount, failCount)
|
||||
|
||||
if failCount > 0 {
|
||||
return fmt.Errorf("some environment variables failed to sync")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
|
||||
cmd.Flags().Bool("is-literal", false, "Treat all values as literal (don't interpolate variables)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
Vendored
+95
@@ -0,0 +1,95 @@
|
||||
package env
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func NewUpdateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update <database_uuid> <env_uuid_or_key>",
|
||||
Short: "Update an environment variable",
|
||||
Long: `Update an existing environment variable. Identify it by UUID or key name.`,
|
||||
Args: cli.ExactArgs(2, "<database_uuid> <env_uuid_or_key>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
dbUUID := args[0]
|
||||
envIdentifier := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
// Check minimum version requirement
|
||||
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.469"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbSvc := service.NewDatabaseService(client)
|
||||
|
||||
// Look up the env var to resolve its key
|
||||
existingEnv, err := dbSvc.GetEnv(ctx, dbUUID, envIdentifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find environment variable '%s': %w", envIdentifier, err)
|
||||
}
|
||||
|
||||
req := &models.DatabaseEnvironmentVariableUpdateRequest{}
|
||||
|
||||
// Use existing key unless --key flag explicitly provides a new one
|
||||
if cmd.Flags().Changed("key") {
|
||||
key, _ := cmd.Flags().GetString("key")
|
||||
req.Key = &key
|
||||
} else {
|
||||
req.Key = &existingEnv.Key
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("value") {
|
||||
value, _ := cmd.Flags().GetString("value")
|
||||
req.Value = &value
|
||||
}
|
||||
if cmd.Flags().Changed("is-literal") {
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
req.IsLiteral = &isLiteral
|
||||
}
|
||||
if cmd.Flags().Changed("is-multiline") {
|
||||
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
|
||||
req.IsMultiline = &isMultiline
|
||||
}
|
||||
if cmd.Flags().Changed("is-shown-once") {
|
||||
isShownOnce, _ := cmd.Flags().GetBool("is-shown-once")
|
||||
req.IsShownOnce = &isShownOnce
|
||||
}
|
||||
if cmd.Flags().Changed("comment") {
|
||||
comment, _ := cmd.Flags().GetString("comment")
|
||||
req.Comment = &comment
|
||||
}
|
||||
|
||||
if req.Value == nil {
|
||||
return fmt.Errorf("--value is required")
|
||||
}
|
||||
|
||||
env, err := dbSvc.UpdateEnv(ctx, dbUUID, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update environment variable: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Environment variable '%s' updated successfully.\n", env.Key)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("key", "", "New environment variable key (rename)")
|
||||
cmd.Flags().String("value", "", "New environment variable value (required)")
|
||||
cmd.Flags().Bool("is-literal", false, "Treat value as literal")
|
||||
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
|
||||
cmd.Flags().Bool("is-shown-once", false, "Only show value once")
|
||||
cmd.Flags().String("comment", "", "Comment for the environment variable")
|
||||
|
||||
return cmd
|
||||
}
|
||||
+12
-2
@@ -32,15 +32,25 @@ func NewGetCommand() *cobra.Command {
|
||||
return fmt.Errorf("failed to get database: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
formatter, err := output.NewFormatter("table", output.Options{
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(database)
|
||||
if err := formatter.Format(database); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !showSensitive && format == output.FormatTable {
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
+15
-2
@@ -30,12 +30,25 @@ func NewListCommand() *cobra.Command {
|
||||
return fmt.Errorf("failed to list databases: %w", err)
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter("table", output.Options{})
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(databases)
|
||||
if err := formatter.Format(databases); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !showSensitive && format == output.FormatTable {
|
||||
fmt.Println("\nNote: Use -s to show sensitive information.")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <db_uuid>",
|
||||
Short: "Create a storage for a database",
|
||||
Long: `Create a persistent volume or file storage for a database.
|
||||
|
||||
Examples:
|
||||
coolify db storage create <db_uuid> --type persistent --name my-volume --mount-path /data
|
||||
coolify db storage create <db_uuid> --type file --mount-path /app/config.yml --content "key: value"`,
|
||||
Args: cli.ExactArgs(1, "<db_uuid>"),
|
||||
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
|
||||
}
|
||||
@@ -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 <db_uuid> <storage_uuid>",
|
||||
Short: "Delete a storage from a database",
|
||||
Long: `Delete a persistent volume or file storage from a database.
|
||||
|
||||
Examples:
|
||||
coolify db storage delete <db_uuid> <storage_uuid>`,
|
||||
Args: cli.ExactArgs(2, "<db_uuid> <storage_uuid>"),
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 <db_uuid>",
|
||||
Short: "List all storages for a database",
|
||||
Long: `List all persistent volumes and file storages for a specific database.`,
|
||||
Args: cli.ExactArgs(1, "<db_uuid>"),
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 <db_uuid>",
|
||||
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 <db_uuid> --uuid <storage_uuid> --type persistent --name my-volume
|
||||
coolify db storage update <db_uuid> --uuid <storage_uuid> --type persistent --is-preview-suffix-enabled`,
|
||||
Args: cli.ExactArgs(1, "<db_uuid>"),
|
||||
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
|
||||
}
|
||||
@@ -27,6 +27,9 @@ Example: coolify deploy batch app1,app2,app3`,
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
if err := validateDeployFlags(ctx, cmd, client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse comma-separated names
|
||||
names := make([]string, 0)
|
||||
@@ -66,7 +69,6 @@ Example: coolify deploy batch app1,app2,app3`,
|
||||
}
|
||||
|
||||
// Deploy all resources
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
|
||||
type result struct {
|
||||
@@ -83,7 +85,7 @@ Example: coolify deploy batch app1,app2,app3`,
|
||||
uuid := nameToUUID[name]
|
||||
fmt.Printf("Deploying %s...\n", name)
|
||||
|
||||
res, err := deploySvc.Deploy(ctx, uuid, force)
|
||||
res, err := deploySvc.Deploy(ctx, getDeployRequest(cmd, uuid))
|
||||
if err != nil {
|
||||
results = append(results, result{
|
||||
Name: name,
|
||||
@@ -126,6 +128,6 @@ Example: coolify deploy batch app1,app2,app3`,
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("force", false, "Force deployment")
|
||||
addDeployFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
package deployment
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/api"
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
)
|
||||
|
||||
const dockerTagMinVersion = "4.0.0-beta.471"
|
||||
|
||||
// NewDeploymentCommand creates the deployment parent command with all subcommands
|
||||
func NewDeploymentCommand() *cobra.Command {
|
||||
@@ -19,3 +29,38 @@ func NewDeploymentCommand() *cobra.Command {
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addDeployFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().Bool("force", false, "Force deployment")
|
||||
cmd.Flags().Int("pull-request-id", 0, "Pull request ID for preview deployments")
|
||||
cmd.Flags().String("docker-tag", "", "Docker image tag override for the deployment")
|
||||
}
|
||||
|
||||
func getDeployRequest(cmd *cobra.Command, uuid string) models.DeployRequest {
|
||||
req := models.DeployRequest{
|
||||
UUID: uuid,
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("force") {
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
req.Force = &force
|
||||
}
|
||||
if cmd.Flags().Changed("pull-request-id") {
|
||||
pullRequestID, _ := cmd.Flags().GetInt("pull-request-id")
|
||||
req.PullRequestID = &pullRequestID
|
||||
}
|
||||
if cmd.Flags().Changed("docker-tag") {
|
||||
dockerTag, _ := cmd.Flags().GetString("docker-tag")
|
||||
req.DockerTag = &dockerTag
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func validateDeployFlags(ctx context.Context, cmd *cobra.Command, client *api.Client) error {
|
||||
if cmd.Flags().Changed("docker-tag") {
|
||||
return cli.CheckMinimumVersion(ctx, client, dockerTagMinVersion)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ func NewNameCommand() *cobra.Command {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
if err := validateDeployFlags(ctx, cmd, client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find resource by name
|
||||
resourceSvc := service.NewResourceService(client)
|
||||
@@ -45,9 +48,8 @@ func NewNameCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
// Deploy using the found UUID
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
result, err := deploySvc.Deploy(ctx, matchedUUID, force)
|
||||
result, err := deploySvc.Deploy(ctx, getDeployRequest(cmd, matchedUUID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy resource: %w", err)
|
||||
}
|
||||
@@ -74,6 +76,6 @@ func NewNameCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("force", false, "Force deployment")
|
||||
addDeployFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -30,10 +30,12 @@ func NewUUIDCommand() *cobra.Command {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
if err := validateDeployFlags(ctx, cmd, client); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
deploySvc := service.NewDeploymentService(client)
|
||||
result, err := deploySvc.Deploy(ctx, uuid, force)
|
||||
result, err := deploySvc.Deploy(ctx, getDeployRequest(cmd, uuid))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deploy resource: %w", err)
|
||||
}
|
||||
@@ -60,6 +62,6 @@ func NewUUIDCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("force", false, "Force deployment")
|
||||
addDeployFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
+531
@@ -4,9 +4,12 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var docsCmd = &cobra.Command{
|
||||
@@ -85,12 +88,540 @@ The markdown files will be written to the specified directory (default: ./docs).
|
||||
},
|
||||
}
|
||||
|
||||
var llmsCmd = &cobra.Command{
|
||||
Use: "llms",
|
||||
Short: "Generate llms.txt and llms-full.txt for AI agents",
|
||||
Long: `Generate AI-friendly documentation files for the Coolify CLI.
|
||||
|
||||
This creates a concise llms.txt quick reference plus a complete llms-full.txt command catalog.
|
||||
The output files will be written to the specified paths (defaults: ./llms.txt and ./llms-full.txt).`,
|
||||
Example: ` coolify docs llms
|
||||
coolify docs llms --output=./llms.txt --full-output=./llms-full.txt`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
outputFile, _ := cmd.Flags().GetString("output")
|
||||
fullOutputFile, _ := cmd.Flags().GetString("full-output")
|
||||
|
||||
return writeLLMsArtifacts(outputFile, fullOutputFile)
|
||||
},
|
||||
}
|
||||
|
||||
const llmsQuickTemplate = `# Coolify CLI - llms.txt
|
||||
|
||||
> Quick AI/LLM instructions for the Coolify CLI.
|
||||
> Source: https://github.com/coollabsio/coolify-cli
|
||||
> API Spec: https://github.com/coollabsio/coolify/blob/v4.x/openapi.json
|
||||
|
||||
## Operating Rules
|
||||
|
||||
- Prefer ` + "`--format json`" + ` for automation and parsing.
|
||||
- Use Coolify UUIDs for resources; do not use internal numeric IDs.
|
||||
- Team commands are the exception: they use numeric team IDs.
|
||||
- Authenticate with a saved context when possible; use ` + "`--token`" + ` only for overrides.
|
||||
- Use ` + "`llms-full.txt`" + ` for the exhaustive command/flag catalog.
|
||||
|
||||
## Installation
|
||||
|
||||
` + "```bash" + `
|
||||
# Linux/macOS (recommended)
|
||||
curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.sh | bash
|
||||
|
||||
# Homebrew (macOS/Linux)
|
||||
brew install coollabsio/coolify-cli/coolify-cli
|
||||
|
||||
# Windows (PowerShell)
|
||||
irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
|
||||
|
||||
# Go install
|
||||
go install github.com/coollabsio/coolify-cli/coolify@latest
|
||||
` + "```" + `
|
||||
|
||||
## Authentication
|
||||
|
||||
1. Get an API token from your Coolify dashboard at ` + "`/security/api-tokens`" + `
|
||||
2. For Coolify Cloud: ` + "`coolify context set-token cloud <token>`" + `
|
||||
3. For self-hosted: ` + "`coolify context add -d <context_name> <url> <token>`" + `
|
||||
4. Switch contexts with ` + "`coolify context use <context_name>`" + `
|
||||
|
||||
## Configuration
|
||||
|
||||
Config file location:
|
||||
- Linux/macOS: ` + "`~/.config/coolify/config.json`" + `
|
||||
- Windows: ` + "`%%APPDATA%%\\coolify\\config.json`" + `
|
||||
|
||||
Supports multiple contexts (instances) with ` + "`coolify context`" + ` commands.
|
||||
|
||||
## Output Formats
|
||||
|
||||
All commands support ` + "`--format`" + ` flag:
|
||||
- ` + "`table`" + ` (default) - human-readable tabular output
|
||||
- ` + "`json`" + ` - compact JSON for scripting
|
||||
- ` + "`pretty`" + ` - indented JSON for debugging
|
||||
|
||||
## Global Flags
|
||||
|
||||
- ` + "`--context <name>`" + ` - use a specific saved context
|
||||
- ` + "`--token <token>`" + ` - override token from config
|
||||
- ` + "`--format table|json|pretty`" + ` - choose output format
|
||||
- ` + "`--show-sensitive`" + ` - reveal sensitive values
|
||||
- ` + "`--debug`" + ` - enable debug output
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Contexts
|
||||
|
||||
` + "```bash" + `
|
||||
coolify context list
|
||||
coolify context verify
|
||||
coolify context version
|
||||
coolify context use prod
|
||||
` + "```" + `
|
||||
|
||||
### Inventory
|
||||
|
||||
` + "```bash" + `
|
||||
coolify server list
|
||||
coolify project list
|
||||
coolify resource list
|
||||
coolify app list
|
||||
coolify service list
|
||||
coolify database list
|
||||
` + "```" + `
|
||||
|
||||
### Applications
|
||||
|
||||
` + "```bash" + `
|
||||
coolify app get <uuid>
|
||||
coolify app start <uuid>
|
||||
coolify app stop <uuid>
|
||||
coolify app restart <uuid>
|
||||
coolify app logs <uuid> --follow
|
||||
coolify app deployments list <app-uuid>
|
||||
coolify app deployments logs <app-uuid> --follow
|
||||
` + "```" + `
|
||||
|
||||
### Environment Variables
|
||||
|
||||
` + "```bash" + `
|
||||
coolify app env list <app-uuid>
|
||||
coolify app env create <app-uuid> --key API_KEY --value secret123
|
||||
coolify app env update <app-uuid> <env-uuid-or-key> --value new-secret
|
||||
coolify app env sync <app-uuid> --file .env.production --build-time --preview
|
||||
` + "```" + `
|
||||
|
||||
### Deployments
|
||||
|
||||
` + "```bash" + `
|
||||
coolify deploy list
|
||||
coolify deploy name my-application
|
||||
coolify deploy batch api,worker,frontend --force
|
||||
coolify deploy cancel <deployment-uuid>
|
||||
` + "```" + `
|
||||
|
||||
### Databases and Services
|
||||
|
||||
` + "```bash" + `
|
||||
coolify database get <uuid>
|
||||
coolify database create postgresql --server-uuid <uuid> --project-uuid <uuid> --environment-name production
|
||||
coolify database backup list <database-uuid>
|
||||
coolify service get <uuid>
|
||||
coolify service create <type> --project-uuid <uuid> --server-uuid <uuid> --instant-deploy
|
||||
` + "```" + `
|
||||
|
||||
## Common Aliases
|
||||
|
||||
- ` + "`coolify app`" + ` | ` + "`coolify apps`" + ` | ` + "`coolify application`" + ` | ` + "`coolify applications`" + `
|
||||
- ` + "`coolify service`" + ` | ` + "`coolify services`" + ` | ` + "`coolify svc`" + `
|
||||
- ` + "`coolify database`" + ` | ` + "`coolify databases`" + ` | ` + "`coolify db`" + ` | ` + "`coolify dbs`" + `
|
||||
- ` + "`coolify teams`" + ` | ` + "`coolify team`" + `
|
||||
|
||||
## Full Reference
|
||||
|
||||
- Full command and parameter catalog: %s
|
||||
- Regenerate docs: ` + "`go run ./coolify docs llms`" + `
|
||||
`
|
||||
|
||||
const llmsFullIntro = `# Coolify CLI - llms-full.txt
|
||||
|
||||
> Full AI/LLM command catalog for the Coolify CLI.
|
||||
> Manage Coolify instances (cloud and self-hosted), servers, projects, applications, databases, services, deployments, domains, and private keys.
|
||||
> Source: https://github.com/coollabsio/coolify-cli
|
||||
> API Spec: https://github.com/coollabsio/coolify/blob/v4.x/openapi.json
|
||||
|
||||
## Companion Files
|
||||
|
||||
- Quick instructions: %s
|
||||
- Regenerate docs: ` + "`go run ./coolify docs llms`" + `
|
||||
|
||||
## Installation
|
||||
|
||||
` + "```bash" + `
|
||||
# Linux/macOS (recommended)
|
||||
curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.sh | bash
|
||||
|
||||
# Homebrew (macOS/Linux)
|
||||
brew install coollabsio/coolify-cli/coolify-cli
|
||||
|
||||
# Windows (PowerShell)
|
||||
irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
|
||||
|
||||
# Go install
|
||||
go install github.com/coollabsio/coolify-cli/coolify@latest
|
||||
` + "```" + `
|
||||
|
||||
## Authentication
|
||||
|
||||
1. Get an API token from your Coolify dashboard at ` + "`/security/api-tokens`" + `
|
||||
2. For Coolify Cloud: ` + "`coolify context set-token cloud <token>`" + `
|
||||
3. For self-hosted: ` + "`coolify context add -d <context_name> <url> <token>`" + `
|
||||
|
||||
## Configuration
|
||||
|
||||
Config file location:
|
||||
- Linux/macOS: ` + "`~/.config/coolify/config.json`" + `
|
||||
- Windows: ` + "`%%APPDATA%%\\coolify\\config.json`" + `
|
||||
|
||||
Supports multiple contexts (instances) with ` + "`coolify context`" + ` commands.
|
||||
|
||||
## Output Formats
|
||||
|
||||
All commands support ` + "`--format`" + ` flag:
|
||||
- ` + "`table`" + ` (default) - human-readable tabular output
|
||||
- ` + "`json`" + ` - compact JSON for scripting
|
||||
- ` + "`pretty`" + ` - indented JSON for debugging
|
||||
`
|
||||
|
||||
const llmsFullBody = `
|
||||
|
||||
## Supported Database Types
|
||||
|
||||
When using ` + "`coolify database create <type>`" + `:
|
||||
- ` + "`postgresql`" + `
|
||||
- ` + "`mysql`" + `
|
||||
- ` + "`mariadb`" + `
|
||||
- ` + "`mongodb`" + `
|
||||
- ` + "`redis`" + `
|
||||
- ` + "`keydb`" + `
|
||||
- ` + "`clickhouse`" + `
|
||||
- ` + "`dragonfly`" + `
|
||||
|
||||
## Usage Examples
|
||||
|
||||
` + "```bash" + `
|
||||
# Multi-context workflow
|
||||
coolify context add prod https://prod.coolify.io <token>
|
||||
coolify context add staging https://staging.coolify.io <token>
|
||||
coolify context use prod
|
||||
coolify --context=staging server list
|
||||
|
||||
# Application lifecycle
|
||||
coolify app list
|
||||
coolify app get <uuid>
|
||||
coolify app start <uuid>
|
||||
coolify app stop <uuid>
|
||||
coolify app restart <uuid>
|
||||
coolify app logs <uuid> --follow
|
||||
|
||||
# Environment variable management
|
||||
coolify app env list <uuid>
|
||||
coolify app env create <uuid> --key API_KEY --value secret123
|
||||
coolify app env sync <uuid> --file .env.production --build-time --preview
|
||||
|
||||
# Deploy workflows
|
||||
coolify deploy name my-application
|
||||
coolify deploy batch api,worker,frontend --force
|
||||
coolify deploy list
|
||||
coolify deploy cancel <uuid>
|
||||
|
||||
# Database backup
|
||||
coolify database backup create <db-uuid> --frequency "0 2 * * *" --enabled --save-s3
|
||||
coolify database backup trigger <db-uuid> <backup-uuid>
|
||||
|
||||
# Application creation
|
||||
coolify app create public --project-uuid <uuid> --server-uuid <uuid> --git-repository https://github.com/user/repo --git-branch main --build-pack nixpacks --ports-exposes 3000
|
||||
coolify app create dockerfile --project-uuid <uuid> --server-uuid <uuid> --dockerfile "FROM node:18\nCOPY . .\nRUN npm install\nCMD [\"node\", \"index.js\"]"
|
||||
coolify app create dockerimage --project-uuid <uuid> --server-uuid <uuid> --docker-registry-image-name nginx --ports-exposes 80
|
||||
|
||||
# Service creation (one-click services)
|
||||
coolify service create <type> --project-uuid <uuid> --server-uuid <uuid> --instant-deploy
|
||||
coolify service create --list-types # list all available service types
|
||||
|
||||
# Storage management
|
||||
coolify app storage create <app-uuid> --type persistent --mount-path /data --name my-volume
|
||||
coolify app storage create <app-uuid> --type file --mount-path /app/config.yml --content "key: value"
|
||||
|
||||
# GitHub App integration
|
||||
coolify github list
|
||||
coolify github repos <app-uuid>
|
||||
coolify github branches <app-uuid> owner/repo
|
||||
|
||||
# Team management
|
||||
coolify team list
|
||||
coolify team current
|
||||
coolify team members list
|
||||
` + "```" + `
|
||||
|
||||
## API Notes
|
||||
|
||||
- All resource identifiers use UUIDs (not internal database IDs)
|
||||
- API base path: ` + "`/api/v1/`" + `
|
||||
- Authentication: Bearer token via ` + "`--token`" + ` flag or context configuration
|
||||
- ` + "`app env sync`" + ` behavior: updates existing variables, creates missing ones, does NOT delete variables not in the file
|
||||
- ` + "`app start`" + ` aliases to ` + "`app deploy`" + ` and also accepts ` + "`--force`" + ` and ` + "`--instant-deploy`" + ` flags
|
||||
- Deployment logs support ` + "`--follow`" + ` for real-time streaming and ` + "`--debuglogs`" + ` for internal operations
|
||||
- ` + "`app logs`" + ` defaults to 100 lines; ` + "`app deployments logs`" + ` defaults to 0 (all lines)
|
||||
- Short flag ` + "`-n`" + ` can be used instead of ` + "`--lines`" + ` for log commands
|
||||
- ` + "`completion`" + ` command supports shells: ` + "`bash`" + `, ` + "`zsh`" + `, ` + "`fish`" + `, ` + "`powershell`" + `
|
||||
- Resource statuses: ` + "`running`" + `, ` + "`stopped`" + `, ` + "`error`" + `
|
||||
- Teams use numeric IDs (not UUIDs) - this is the only resource that uses IDs
|
||||
- Fields marked ` + "`sensitive:\"true\"`" + ` (tokens, passwords, IPs, emails) are hidden by default; use ` + "`--show-sensitive`" + ` to reveal
|
||||
|
||||
---
|
||||
|
||||
## Command Reference
|
||||
|
||||
`
|
||||
|
||||
func buildQuickLLMSText(fullReferencePath string) string {
|
||||
return fmt.Sprintf(llmsQuickTemplate, fullReferencePath)
|
||||
}
|
||||
|
||||
func buildFullLLMSText(quickReferencePath string) string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, llmsFullIntro, quickReferencePath)
|
||||
writeLLMsAliases(&sb, rootCmd, "coolify")
|
||||
sb.WriteString(llmsFullBody)
|
||||
writeLLMsCommand(&sb, rootCmd, "coolify")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func writeLLMsArtifacts(outputFile, fullOutputFile string) error {
|
||||
if filepath.Clean(outputFile) == filepath.Clean(fullOutputFile) {
|
||||
return fmt.Errorf("output and full-output must be different files")
|
||||
}
|
||||
|
||||
if err := ensureParentDir(outputFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureParentDir(fullOutputFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quickReferencePath := llmsReferencePath(fullOutputFile, outputFile)
|
||||
fullReferencePath := llmsReferencePath(outputFile, fullOutputFile)
|
||||
|
||||
if err := os.WriteFile(outputFile, []byte(buildQuickLLMSText(fullReferencePath)), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write llms.txt: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(fullOutputFile, []byte(buildFullLLMSText(quickReferencePath)), 0600); err != nil {
|
||||
return fmt.Errorf("failed to write llms-full.txt: %w", err)
|
||||
}
|
||||
|
||||
absQuickPath, _ := filepath.Abs(outputFile)
|
||||
absFullPath, _ := filepath.Abs(fullOutputFile)
|
||||
fmt.Printf("llms.txt generated successfully: %s\n", absQuickPath)
|
||||
fmt.Printf("llms-full.txt generated successfully: %s\n", absFullPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureParentDir(path string) error {
|
||||
parentDir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(parentDir, 0750); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func llmsReferencePath(fromFile, toFile string) string {
|
||||
referencePath, err := filepath.Rel(filepath.Dir(fromFile), toFile)
|
||||
if err != nil {
|
||||
return filepath.ToSlash(toFile)
|
||||
}
|
||||
referencePath = filepath.ToSlash(referencePath)
|
||||
if strings.HasPrefix(referencePath, ".") || strings.HasPrefix(referencePath, "/") {
|
||||
return referencePath
|
||||
}
|
||||
return "./" + referencePath
|
||||
}
|
||||
|
||||
// writeLLMsAliases writes aliases derived from the Cobra command tree.
|
||||
func writeLLMsAliases(sb *strings.Builder, cmd *cobra.Command, parentPath string) {
|
||||
aliases := collectLLMsAliases(cmd, parentPath)
|
||||
if len(aliases) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sb.WriteString("\n## Command Aliases\n\n")
|
||||
sb.WriteString("Aliases are derived from the CLI command tree:\n")
|
||||
for _, aliasLine := range aliases {
|
||||
fmt.Fprintf(sb, "- %s\n", aliasLine)
|
||||
}
|
||||
}
|
||||
|
||||
func collectLLMsAliases(cmd *cobra.Command, parentPath string) []string {
|
||||
var aliases []string
|
||||
if cmd.Name() != "docs" && cmd.Name() != "help" {
|
||||
if len(cmd.Aliases) > 0 {
|
||||
aliasNames := append([]string{cmd.Name()}, cmd.Aliases...)
|
||||
for i := range aliasNames {
|
||||
aliasNames[i] = fmt.Sprintf("`%s`", commandPathPrefix(parentPath, cmd)+aliasNames[i])
|
||||
}
|
||||
aliases = append(aliases, strings.Join(aliasNames, " | "))
|
||||
}
|
||||
}
|
||||
|
||||
for _, child := range cmd.Commands() {
|
||||
if child.Hidden || child.Name() == "help" {
|
||||
continue
|
||||
}
|
||||
aliases = append(aliases, collectLLMsAliases(child, llmsCommandName(parentPath, cmd))...)
|
||||
}
|
||||
|
||||
slices.Sort(aliases)
|
||||
return slices.Compact(aliases)
|
||||
}
|
||||
|
||||
func llmsCommandName(parentPath string, cmd *cobra.Command) string {
|
||||
if !cmd.HasParent() {
|
||||
return parentPath
|
||||
}
|
||||
|
||||
parts := strings.Fields(cmd.Use)
|
||||
commandPath := parentPath + " " + parts[0]
|
||||
if len(parts) > 1 {
|
||||
commandPath += " " + strings.Join(parts[1:], " ")
|
||||
}
|
||||
return commandPath
|
||||
}
|
||||
|
||||
func commandPathPrefix(parentPath string, cmd *cobra.Command) string {
|
||||
if cmd.HasParent() {
|
||||
return parentPath + " "
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// writeLLMsCommand recursively writes command documentation in llms.txt format.
|
||||
func writeLLMsCommand(sb *strings.Builder, cmd *cobra.Command, parentPath string) {
|
||||
// Build the full command path including args from Use field
|
||||
commandPath := llmsCommandName(parentPath, cmd)
|
||||
|
||||
// Skip the docs command itself and help command
|
||||
if cmd.Name() == "docs" || cmd.Name() == "help" {
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if this command should be written
|
||||
isRoot := !cmd.HasParent()
|
||||
isRunnable := cmd.RunE != nil || cmd.Run != nil
|
||||
hasVisibleChildren := false
|
||||
for _, child := range cmd.Commands() {
|
||||
if !child.Hidden && child.Name() != "help" {
|
||||
hasVisibleChildren = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Write the root command, runnable commands, and leaf commands (no children)
|
||||
if isRoot || isRunnable || !hasVisibleChildren {
|
||||
// Get description - prefer Long if it's a single clean sentence, otherwise use Short
|
||||
description := cmd.Short
|
||||
if cmd.Long != "" {
|
||||
longLines := strings.Split(strings.TrimSpace(cmd.Long), "\n")
|
||||
if len(longLines) == 1 && len(longLines[0]) < 200 {
|
||||
description = longLines[0]
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(sb, "Command: %s\n", commandPath)
|
||||
fmt.Fprintf(sb, "Description: %s\n", description)
|
||||
|
||||
// For root command, show persistent flags; for others, show local flags
|
||||
var flags []*pflag.Flag
|
||||
if isRoot {
|
||||
cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Name == "help" {
|
||||
return
|
||||
}
|
||||
flags = append(flags, f)
|
||||
})
|
||||
} else {
|
||||
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
|
||||
if f.Name == "help" {
|
||||
return
|
||||
}
|
||||
flags = append(flags, f)
|
||||
})
|
||||
}
|
||||
|
||||
if len(flags) == 0 {
|
||||
sb.WriteString("Parameters: (None)\n")
|
||||
} else {
|
||||
sb.WriteString("Parameters:\n")
|
||||
for _, f := range flags {
|
||||
flagType := f.Value.Type()
|
||||
// Normalize type names
|
||||
switch flagType {
|
||||
case "int", "int32", "int64":
|
||||
flagType = "integer"
|
||||
case "bool":
|
||||
flagType = "boolean"
|
||||
}
|
||||
|
||||
// Check if the flag is marked as required via cobra annotation
|
||||
// or via "(required)" in the usage string
|
||||
required := isFlagRequired(f)
|
||||
|
||||
if f.Shorthand != "" {
|
||||
fmt.Fprintf(sb, " - name: --%s (-%s)\n", f.Name, f.Shorthand)
|
||||
} else {
|
||||
fmt.Fprintf(sb, " - name: --%s\n", f.Name)
|
||||
}
|
||||
fmt.Fprintf(sb, " type: %s\n", flagType)
|
||||
fmt.Fprintf(sb, " description: %s\n", f.Usage)
|
||||
fmt.Fprintf(sb, " required: %t\n", required)
|
||||
if f.DefValue != "" && f.DefValue != "[]" {
|
||||
fmt.Fprintf(sb, " default: %s\n", f.DefValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Recurse into subcommands
|
||||
for _, child := range cmd.Commands() {
|
||||
if child.Hidden || child.Name() == "help" {
|
||||
continue
|
||||
}
|
||||
childPath := parentPath
|
||||
if cmd.HasParent() {
|
||||
childPath = llmsCommandName(parentPath, cmd)
|
||||
}
|
||||
writeLLMsCommand(sb, child, childPath)
|
||||
}
|
||||
}
|
||||
|
||||
// isFlagRequired checks if a flag is required by looking at cobra annotations
|
||||
// and the "(required)" convention in usage strings.
|
||||
func isFlagRequired(f *pflag.Flag) bool {
|
||||
// Check cobra's MarkFlagRequired annotation
|
||||
if ann, ok := f.Annotations[cobra.BashCompOneRequiredFlag]; ok && len(ann) > 0 && ann[0] == "true" {
|
||||
return true
|
||||
}
|
||||
// Check for "(required)" in usage string (convention used in this codebase)
|
||||
return strings.Contains(strings.ToLower(f.Usage), "(required)")
|
||||
}
|
||||
|
||||
func NewDocsCommand() *cobra.Command {
|
||||
docsCmd.AddCommand(manCmd)
|
||||
docsCmd.AddCommand(markdownCmd)
|
||||
docsCmd.AddCommand(llmsCmd)
|
||||
|
||||
manCmd.Flags().StringP("output-dir", "o", "./man", "Output directory for man pages")
|
||||
markdownCmd.Flags().StringP("output-dir", "o", "./docs", "Output directory for markdown files")
|
||||
llmsCmd.Flags().StringP("output", "o", "./llms.txt", "Output file path")
|
||||
llmsCmd.Flags().String("full-output", "./llms-full.txt", "Full output file path")
|
||||
|
||||
return docsCmd
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestWriteLLMsCommandIncludesShorthandAndDefaults(t *testing.T) {
|
||||
root := &cobra.Command{Use: "coolify"}
|
||||
child := &cobra.Command{
|
||||
Use: "logs <uuid>",
|
||||
Short: "Show logs",
|
||||
Run: func(_ *cobra.Command, _ []string) {},
|
||||
}
|
||||
child.Flags().IntP("lines", "n", 0, "Number of log lines to display (0 = all)")
|
||||
child.Flags().Bool("verbose", false, "Verbose output")
|
||||
child.Flags().Bool("enabled", true, "Enabled by default")
|
||||
root.AddCommand(child)
|
||||
|
||||
var sb strings.Builder
|
||||
writeLLMsCommand(&sb, child, "coolify")
|
||||
got := sb.String()
|
||||
|
||||
for _, want := range []string{
|
||||
"Command: coolify logs <uuid>",
|
||||
" - name: --lines (-n)",
|
||||
" default: 0",
|
||||
" - name: --verbose",
|
||||
" default: false",
|
||||
" - name: --enabled",
|
||||
" default: true",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected output to contain %q\nfull output:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteLLMsAliasesUsesCommandTree(t *testing.T) {
|
||||
root := &cobra.Command{Use: "coolify"}
|
||||
teams := &cobra.Command{Use: "teams", Aliases: []string{"team"}}
|
||||
members := &cobra.Command{Use: "members", Aliases: []string{"member"}}
|
||||
start := &cobra.Command{
|
||||
Use: "start <uuid>",
|
||||
Aliases: []string{"deploy"},
|
||||
}
|
||||
|
||||
root.AddCommand(teams)
|
||||
root.AddCommand(start)
|
||||
teams.AddCommand(members)
|
||||
|
||||
var sb strings.Builder
|
||||
writeLLMsAliases(&sb, root, "coolify")
|
||||
got := sb.String()
|
||||
|
||||
for _, want := range []string{
|
||||
"## Command Aliases",
|
||||
"`coolify start` | `coolify deploy`",
|
||||
"`coolify teams` | `coolify team`",
|
||||
"`coolify teams members` | `coolify teams member`",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected alias output to contain %q\nfull output:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildQuickLLMSTextIncludesCoreGuidance(t *testing.T) {
|
||||
got := buildQuickLLMSText("./llms-full.txt")
|
||||
|
||||
for _, want := range []string{
|
||||
"# Coolify CLI - llms.txt",
|
||||
"Prefer `--format json` for automation and parsing.",
|
||||
"coolify context verify",
|
||||
"coolify app logs <uuid> --follow",
|
||||
"Full command and parameter catalog: ./llms-full.txt",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("expected quick llms output to contain %q\nfull output:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteLLMsArtifactsWritesQuickAndFullFiles(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
quickPath := filepath.Join(tempDir, "llms.txt")
|
||||
fullPath := filepath.Join(tempDir, "nested", "llms-full.txt")
|
||||
|
||||
if err := writeLLMsArtifacts(quickPath, fullPath); err != nil {
|
||||
t.Fatalf("writeLLMsArtifacts() error = %v", err)
|
||||
}
|
||||
|
||||
quickContent, err := os.ReadFile(quickPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed reading quick file: %v", err)
|
||||
}
|
||||
fullContent, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed reading full file: %v", err)
|
||||
}
|
||||
|
||||
for _, want := range []struct {
|
||||
content string
|
||||
substr string
|
||||
}{
|
||||
{string(quickContent), "./nested/llms-full.txt"},
|
||||
{string(fullContent), "../llms.txt"},
|
||||
{string(fullContent), "## Command Reference"},
|
||||
} {
|
||||
if !strings.Contains(want.content, want.substr) {
|
||||
t.Fatalf("expected generated content to contain %q", want.substr)
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+7
-2
@@ -57,14 +57,18 @@ func NewCreateCommand() *cobra.Command {
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
if cmd.Flags().Changed("comment") {
|
||||
comment, _ := cmd.Flags().GetString("comment")
|
||||
req.Comment = &comment
|
||||
}
|
||||
|
||||
serviceSvc := service.NewService(client)
|
||||
env, err := serviceSvc.CreateEnv(ctx, uuid, req)
|
||||
_, err = serviceSvc.CreateEnv(ctx, uuid, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create environment variable: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Environment variable '%s' created successfully.\n", env.Key)
|
||||
fmt.Printf("Environment variable '%s' created successfully.\n", key)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -75,6 +79,7 @@ func NewCreateCommand() *cobra.Command {
|
||||
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
|
||||
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
|
||||
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
|
||||
cmd.Flags().String("comment", "", "Comment for the environment variable")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
Vendored
+23
-10
@@ -12,13 +12,14 @@ import (
|
||||
|
||||
func NewUpdateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update <service_uuid>",
|
||||
Use: "update <service_uuid> <env_uuid_or_key>",
|
||||
Short: "Update an environment variable",
|
||||
Long: `Update an existing environment variable. UUID is the service.`,
|
||||
Args: cli.ExactArgs(1, "<service_uuid>"),
|
||||
Long: `Update an existing environment variable. Identify it by UUID or key name.`,
|
||||
Args: cli.ExactArgs(2, "<service_uuid> <env_uuid_or_key>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
serviceUUID := args[0]
|
||||
envIdentifier := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
@@ -30,13 +31,24 @@ func NewUpdateCommand() *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceSvc := service.NewService(client)
|
||||
|
||||
// Look up the env var to resolve its key
|
||||
existingEnv, err := serviceSvc.GetEnv(ctx, serviceUUID, envIdentifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find environment variable '%s': %w", envIdentifier, err)
|
||||
}
|
||||
|
||||
req := &models.ServiceEnvironmentVariableUpdateRequest{}
|
||||
|
||||
// Only set fields that were provided
|
||||
// Use existing key unless --key flag explicitly provides a new one
|
||||
if cmd.Flags().Changed("key") {
|
||||
key, _ := cmd.Flags().GetString("key")
|
||||
req.Key = &key
|
||||
} else {
|
||||
req.Key = &existingEnv.Key
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("value") {
|
||||
value, _ := cmd.Flags().GetString("value")
|
||||
req.Value = &value
|
||||
@@ -57,15 +69,15 @@ func NewUpdateCommand() *cobra.Command {
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
if req.Key == nil {
|
||||
return fmt.Errorf("--key is required")
|
||||
if cmd.Flags().Changed("comment") {
|
||||
comment, _ := cmd.Flags().GetString("comment")
|
||||
req.Comment = &comment
|
||||
}
|
||||
|
||||
if req.Value == nil {
|
||||
return fmt.Errorf("--value is required")
|
||||
}
|
||||
|
||||
serviceSvc := service.NewService(client)
|
||||
env, err := serviceSvc.UpdateEnv(ctx, serviceUUID, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update environment variable: %w", err)
|
||||
@@ -76,12 +88,13 @@ func NewUpdateCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("key", "", "New environment variable key")
|
||||
cmd.Flags().String("value", "", "New environment variable value")
|
||||
cmd.Flags().String("key", "", "New environment variable key (rename)")
|
||||
cmd.Flags().String("value", "", "New environment variable value (required)")
|
||||
cmd.Flags().Bool("build-time", true, "Available at build time (default: true)")
|
||||
cmd.Flags().Bool("is-literal", false, "Treat value as literal (don't interpolate variables)")
|
||||
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
|
||||
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
|
||||
cmd.Flags().String("comment", "", "Comment for the environment variable")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+6
-1
@@ -32,7 +32,12 @@ func NewGetCommand() *cobra.Command {
|
||||
return fmt.Errorf("failed to get service: %w", err)
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter("table", output.Options{})
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
+6
-1
@@ -30,7 +30,12 @@ func NewListCommand() *cobra.Command {
|
||||
return fmt.Errorf("failed to list services: %w", err)
|
||||
}
|
||||
|
||||
formatter, err := output.NewFormatter("table", output.Options{})
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
formatter, err := output.NewFormatter(format, output.Options{
|
||||
ShowSensitive: showSensitive,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 <service_uuid>",
|
||||
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 <service_uuid> --resource-uuid <sub_resource_uuid> --type persistent --name my-volume --mount-path /data
|
||||
coolify svc storage create <service_uuid> --resource-uuid <sub_resource_uuid> --type file --mount-path /app/config.yml --content "key: value"`,
|
||||
Args: cli.ExactArgs(1, "<service_uuid>"),
|
||||
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
|
||||
}
|
||||
@@ -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 <service_uuid> <storage_uuid>",
|
||||
Short: "Delete a storage from a service",
|
||||
Long: `Delete a persistent volume or file storage from a service.
|
||||
|
||||
Examples:
|
||||
coolify svc storage delete <service_uuid> <storage_uuid>`,
|
||||
Args: cli.ExactArgs(2, "<service_uuid> <storage_uuid>"),
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 <service_uuid>",
|
||||
Short: "List all storages for a service",
|
||||
Long: `List all persistent volumes and file storages for a specific service.`,
|
||||
Args: cli.ExactArgs(1, "<service_uuid>"),
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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 <service_uuid>",
|
||||
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 <service_uuid> --uuid <storage_uuid> --type persistent --name my-volume
|
||||
coolify svc storage update <service_uuid> --uuid <storage_uuid> --type persistent --is-preview-suffix-enabled`,
|
||||
Args: cli.ExactArgs(1, "<service_uuid>"),
|
||||
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
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
module github.com/coollabsio/coolify-cli
|
||||
|
||||
go 1.24.6
|
||||
go 1.24.13
|
||||
|
||||
require (
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/creativeprojects/go-selfupdate v1.5.1
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/olekukonko/tablewriter v1.1.2
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
@@ -15,9 +17,13 @@ require (
|
||||
code.gitea.io/sdk/gitea v0.22.0 // indirect
|
||||
github.com/42wim/httpsig v1.2.3 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.6.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
@@ -26,13 +32,18 @@ require (
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||
|
||||
@@ -6,6 +6,12 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
@@ -50,8 +56,19 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.3 h1:sV2jrhQGq5B3W0nENUISCR6azIPf7UBUpVq0x/y70Fg=
|
||||
github.com/olekukonko/ll v0.1.3/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
|
||||
github.com/olekukonko/tablewriter v1.1.2 h1:L2kI1Y5tZBct/O/TyZK1zIE9GlBj/TVs+AY5tZDCDSc=
|
||||
github.com/olekukonko/tablewriter v1.1.2/go.mod h1:z7SYPugVqGVavWoA2sGsFIoOVNmEHxUAAMrhXONtfkg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
@@ -97,6 +114,8 @@ golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
||||
+249
-16
@@ -1,16 +1,116 @@
|
||||
package models
|
||||
|
||||
// ApplicationSettings represents the settings for a Coolify application
|
||||
type ApplicationSettings struct {
|
||||
ID int `json:"-" table:"-"`
|
||||
ApplicationID int `json:"-" table:"-"`
|
||||
IsStatic *bool `json:"is_static,omitempty"`
|
||||
IsBuildServerEnabled *bool `json:"is_build_server_enabled,omitempty"`
|
||||
IsPreserveRepositoryEnabled *bool `json:"is_preserve_repository_enabled,omitempty"`
|
||||
IsAutoDeployEnabled *bool `json:"is_auto_deploy_enabled,omitempty"`
|
||||
IsForceHTTPSEnabled *bool `json:"is_force_https_enabled,omitempty"`
|
||||
IsDebugEnabled *bool `json:"is_debug_enabled,omitempty"`
|
||||
IsPreviewDeploymentsEnabled *bool `json:"is_preview_deployments_enabled,omitempty"`
|
||||
IsGitSubmodulesEnabled *bool `json:"is_git_submodules_enabled,omitempty"`
|
||||
IsGitLFSEnabled *bool `json:"is_git_lfs_enabled,omitempty"`
|
||||
CreatedAt string `json:"-" table:"-"`
|
||||
UpdatedAt string `json:"-" table:"-"`
|
||||
}
|
||||
|
||||
// Application represents a Coolify application
|
||||
type Application struct {
|
||||
ID int `json:"-" table:"-"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Status string `json:"status"`
|
||||
GitBranch *string `json:"git_branch,omitempty"`
|
||||
FQDN *string `json:"fqdn,omitempty"`
|
||||
CreatedAt string `json:"-" table:"-"`
|
||||
UpdatedAt string `json:"-" table:"-"`
|
||||
// Table-visible fields
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Status string `json:"status"`
|
||||
FQDN *string `json:"fqdn,omitempty"`
|
||||
GitRepository *string `json:"git_repository,omitempty"`
|
||||
GitBranch *string `json:"git_branch,omitempty"`
|
||||
BuildPack *string `json:"build_pack,omitempty"`
|
||||
PortsExposes *string `json:"ports_exposes,omitempty"`
|
||||
|
||||
// Git extended (JSON-only)
|
||||
GitCommitSHA *string `json:"git_commit_sha,omitempty" table:"-"`
|
||||
GitFullURL *string `json:"git_full_url,omitempty" table:"-"`
|
||||
|
||||
// Build configuration (JSON-only)
|
||||
InstallCommand *string `json:"install_command,omitempty" table:"-"`
|
||||
BuildCommand *string `json:"build_command,omitempty" table:"-"`
|
||||
StartCommand *string `json:"start_command,omitempty" table:"-"`
|
||||
BaseDirectory *string `json:"base_directory,omitempty" table:"-"`
|
||||
PublishDirectory *string `json:"publish_directory,omitempty" table:"-"`
|
||||
StaticImage *string `json:"static_image,omitempty" table:"-"`
|
||||
|
||||
// Docker configuration (JSON-only)
|
||||
Dockerfile *string `json:"dockerfile,omitempty" table:"-"`
|
||||
DockerfileLocation *string `json:"dockerfile_location,omitempty" table:"-"`
|
||||
DockerRegistryImageName *string `json:"docker_registry_image_name,omitempty" table:"-"`
|
||||
DockerRegistryImageTag *string `json:"docker_registry_image_tag,omitempty" table:"-"`
|
||||
DockerCompose *string `json:"docker_compose,omitempty" table:"-"`
|
||||
DockerComposeRaw *string `json:"docker_compose_raw,omitempty" table:"-"`
|
||||
DockerComposeLocation *string `json:"docker_compose_location,omitempty" table:"-"`
|
||||
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty" table:"-"`
|
||||
CustomLabels *string `json:"custom_labels,omitempty" table:"-"`
|
||||
CustomNginxConfiguration *string `json:"custom_nginx_configuration,omitempty" table:"-"`
|
||||
|
||||
// Networking (JSON-only)
|
||||
PortsMappings *string `json:"ports_mappings,omitempty" table:"-"`
|
||||
Domains *string `json:"domains,omitempty" table:"-"`
|
||||
Redirect *string `json:"redirect,omitempty" table:"-"`
|
||||
PreviewURLTemplate *string `json:"preview_url_template,omitempty" table:"-"`
|
||||
|
||||
// Health checks (JSON-only)
|
||||
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty" table:"-"`
|
||||
HealthCheckPath *string `json:"health_check_path,omitempty" table:"-"`
|
||||
HealthCheckPort *string `json:"health_check_port,omitempty" table:"-"`
|
||||
HealthCheckHost *string `json:"health_check_host,omitempty" table:"-"`
|
||||
HealthCheckMethod *string `json:"health_check_method,omitempty" table:"-"`
|
||||
HealthCheckScheme *string `json:"health_check_scheme,omitempty" table:"-"`
|
||||
HealthCheckReturnCode *int `json:"health_check_return_code,omitempty" table:"-"`
|
||||
HealthCheckResponseText *string `json:"health_check_response_text,omitempty" table:"-"`
|
||||
HealthCheckInterval *int `json:"health_check_interval,omitempty" table:"-"`
|
||||
HealthCheckTimeout *int `json:"health_check_timeout,omitempty" table:"-"`
|
||||
HealthCheckRetries *int `json:"health_check_retries,omitempty" table:"-"`
|
||||
HealthCheckStartPeriod *int `json:"health_check_start_period,omitempty" table:"-"`
|
||||
|
||||
// Resource limits (JSON-only)
|
||||
LimitsCPUs *string `json:"limits_cpus,omitempty" table:"-"`
|
||||
LimitsCPUShares *int `json:"limits_cpu_shares,omitempty" table:"-"`
|
||||
LimitsCPUSet *string `json:"limits_cpuset,omitempty" table:"-"`
|
||||
LimitsMemory *string `json:"limits_memory,omitempty" table:"-"`
|
||||
LimitsMemoryReservation *string `json:"limits_memory_reservation,omitempty" table:"-"`
|
||||
LimitsMemorySwap *string `json:"limits_memory_swap,omitempty" table:"-"`
|
||||
LimitsMemorySwappiness *int `json:"limits_memory_swappiness,omitempty" table:"-"`
|
||||
|
||||
// Deployment hooks (JSON-only)
|
||||
PreDeploymentCommand *string `json:"pre_deployment_command,omitempty" table:"-"`
|
||||
PreDeploymentCommandContainer *string `json:"pre_deployment_command_container,omitempty" table:"-"`
|
||||
PostDeploymentCommand *string `json:"post_deployment_command,omitempty" table:"-"`
|
||||
PostDeploymentCommandContainer *string `json:"post_deployment_command_container,omitempty" table:"-"`
|
||||
|
||||
// Webhook secrets (JSON-only)
|
||||
ManualWebhookSecretGitHub *string `json:"manual_webhook_secret_github,omitempty" table:"-" sensitive:"true"`
|
||||
ManualWebhookSecretGitLab *string `json:"manual_webhook_secret_gitlab,omitempty" table:"-" sensitive:"true"`
|
||||
ManualWebhookSecretBitbucket *string `json:"manual_webhook_secret_bitbucket,omitempty" table:"-" sensitive:"true"`
|
||||
ManualWebhookSecretGitea *string `json:"manual_webhook_secret_gitea,omitempty" table:"-" sensitive:"true"`
|
||||
|
||||
// Misc (JSON-only)
|
||||
WatchPaths *string `json:"watch_paths,omitempty" table:"-"`
|
||||
SwarmReplicas *int `json:"swarm_replicas,omitempty" table:"-"`
|
||||
ConfigHash *string `json:"config_hash,omitempty" table:"-"`
|
||||
|
||||
// Nested settings (JSON-only)
|
||||
Settings *ApplicationSettings `json:"settings,omitempty" table:"-"`
|
||||
|
||||
// Hidden fields (not in JSON or table output)
|
||||
ID int `json:"-" table:"-"`
|
||||
EnvironmentID *int `json:"-" table:"-"`
|
||||
DestinationID *int `json:"-" table:"-"`
|
||||
SourceID *int `json:"-" table:"-"`
|
||||
PrivateKeyID *int `json:"-" table:"-"`
|
||||
CreatedAt string `json:"-" table:"-"`
|
||||
UpdatedAt string `json:"-" table:"-"`
|
||||
}
|
||||
|
||||
// ApplicationListItem represents a simplified application for list view
|
||||
@@ -43,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"`
|
||||
@@ -106,6 +207,7 @@ type EnvironmentVariable struct {
|
||||
IsShownOnce bool `json:"is_shown_once"`
|
||||
IsRuntime bool `json:"is_runtime"`
|
||||
IsShared bool `json:"is_shared"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
RealValue *string `json:"real_value,omitempty" sensitive:"true"`
|
||||
ApplicationID *int `json:"-" table:"-"`
|
||||
CreatedAt string `json:"-" table:"-"`
|
||||
@@ -114,13 +216,14 @@ type EnvironmentVariable struct {
|
||||
|
||||
// EnvironmentVariableCreateRequest represents the request to create an environment variable
|
||||
type EnvironmentVariableCreateRequest struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
IsBuildTime *bool `json:"is_build_time,omitempty"`
|
||||
IsPreview *bool `json:"is_preview,omitempty"`
|
||||
IsLiteral *bool `json:"is_literal,omitempty"`
|
||||
IsMultiline *bool `json:"is_multiline,omitempty"`
|
||||
IsRuntime *bool `json:"is_runtime,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
IsBuildTime *bool `json:"is_build_time,omitempty"`
|
||||
IsPreview *bool `json:"is_preview,omitempty"`
|
||||
IsLiteral *bool `json:"is_literal,omitempty"`
|
||||
IsMultiline *bool `json:"is_multiline,omitempty"`
|
||||
IsRuntime *bool `json:"is_runtime,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// EnvironmentVariableUpdateRequest represents the request to update an environment variable
|
||||
@@ -132,6 +235,131 @@ type EnvironmentVariableUpdateRequest struct {
|
||||
IsLiteral *bool `json:"is_literal,omitempty"`
|
||||
IsMultiline *bool `json:"is_multiline,omitempty"`
|
||||
IsRuntime *bool `json:"is_runtime,omitempty"`
|
||||
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
|
||||
@@ -164,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"`
|
||||
@@ -207,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"`
|
||||
@@ -246,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"`
|
||||
@@ -276,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"`
|
||||
@@ -307,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"`
|
||||
|
||||
@@ -164,6 +164,52 @@ type DatabaseLifecycleResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// DatabaseEnvironmentVariable represents an environment variable for a database
|
||||
type DatabaseEnvironmentVariable struct {
|
||||
ID int `json:"-" table:"-"`
|
||||
UUID string `json:"uuid"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value" sensitive:"true"`
|
||||
IsBuildTime bool `json:"is_buildtime"`
|
||||
IsLiteralValue bool `json:"is_literal"`
|
||||
IsShownOnce bool `json:"is_shown_once"`
|
||||
IsRuntime bool `json:"is_runtime"`
|
||||
IsShared bool `json:"is_shared"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
RealValue *string `json:"real_value,omitempty" sensitive:"true"`
|
||||
DatabaseID *int `json:"-" table:"-"`
|
||||
CreatedAt string `json:"-" table:"-"`
|
||||
UpdatedAt string `json:"-" table:"-"`
|
||||
}
|
||||
|
||||
// DatabaseEnvironmentVariableCreateRequest represents the request to create a database environment variable
|
||||
type DatabaseEnvironmentVariableCreateRequest struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
IsLiteral *bool `json:"is_literal,omitempty"`
|
||||
IsMultiline *bool `json:"is_multiline,omitempty"`
|
||||
IsShownOnce *bool `json:"is_shown_once,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseEnvironmentVariableUpdateRequest represents the request to update a database environment variable
|
||||
type DatabaseEnvironmentVariableUpdateRequest struct {
|
||||
Key *string `json:"key,omitempty"`
|
||||
Value *string `json:"value,omitempty"`
|
||||
IsLiteral *bool `json:"is_literal,omitempty"`
|
||||
IsMultiline *bool `json:"is_multiline,omitempty"`
|
||||
IsShownOnce *bool `json:"is_shown_once,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// DatabaseEnvBulkUpdateRequest represents the request to bulk update database environment variables
|
||||
type DatabaseEnvBulkUpdateRequest struct {
|
||||
Data []DatabaseEnvironmentVariableCreateRequest `json:"data"`
|
||||
}
|
||||
|
||||
// DatabaseEnvBulkUpdateResponse represents the response from database bulk update
|
||||
type DatabaseEnvBulkUpdateResponse []DatabaseEnvironmentVariable
|
||||
|
||||
// DatabaseBackup represents a scheduled database backup configuration
|
||||
type DatabaseBackup struct {
|
||||
ID int `json:"-" table:"-"`
|
||||
|
||||
@@ -23,3 +23,11 @@ type DeployResponse struct {
|
||||
Message string `json:"message"`
|
||||
DeploymentUUID string `json:"deployment_uuid,omitempty"`
|
||||
}
|
||||
|
||||
// DeployRequest represents the request to trigger a deployment.
|
||||
type DeployRequest struct {
|
||||
UUID string `json:"uuid"`
|
||||
Force *bool `json:"force,omitempty"`
|
||||
PullRequestID *int `json:"pull_request_id,omitempty"`
|
||||
DockerTag *string `json:"docker_tag,omitempty"`
|
||||
}
|
||||
|
||||
@@ -99,6 +99,126 @@ func TestProject_UnmarshalFromFixture(t *testing.T) {
|
||||
assert.Equal(t, "running", project.Environments[0].Applications[0].Status)
|
||||
}
|
||||
|
||||
func TestApplication_MarshalUnmarshal(t *testing.T) {
|
||||
desc := "Test application"
|
||||
repo := "https://github.com/example/app"
|
||||
branch := "main"
|
||||
fqdn := "https://app.example.com"
|
||||
buildPack := "nixpacks"
|
||||
ports := "3000"
|
||||
installCmd := "npm install"
|
||||
buildCmd := "npm run build"
|
||||
startCmd := "npm start"
|
||||
limitsCPUs := "2"
|
||||
limitsMemory := "512M"
|
||||
healthPath := "/health"
|
||||
isAutoDeployEnabled := true
|
||||
|
||||
app := Application{
|
||||
ID: 1,
|
||||
UUID: "app-uuid",
|
||||
Name: "My App",
|
||||
Description: &desc,
|
||||
Status: "running",
|
||||
FQDN: &fqdn,
|
||||
GitRepository: &repo,
|
||||
GitBranch: &branch,
|
||||
BuildPack: &buildPack,
|
||||
PortsExposes: &ports,
|
||||
InstallCommand: &installCmd,
|
||||
BuildCommand: &buildCmd,
|
||||
StartCommand: &startCmd,
|
||||
LimitsCPUs: &limitsCPUs,
|
||||
LimitsMemory: &limitsMemory,
|
||||
HealthCheckPath: &healthPath,
|
||||
Settings: &ApplicationSettings{
|
||||
IsAutoDeployEnabled: &isAutoDeployEnabled,
|
||||
},
|
||||
CreatedAt: "2024-01-01T00:00:00Z",
|
||||
UpdatedAt: "2024-01-02T00:00:00Z",
|
||||
}
|
||||
|
||||
// Marshal
|
||||
data, err := json.Marshal(app)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Unmarshal
|
||||
var unmarshaled Application
|
||||
err = json.Unmarshal(data, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, app.UUID, unmarshaled.UUID)
|
||||
assert.Equal(t, app.Name, unmarshaled.Name)
|
||||
assert.Equal(t, *app.GitRepository, *unmarshaled.GitRepository)
|
||||
assert.Equal(t, *app.BuildPack, *unmarshaled.BuildPack)
|
||||
assert.Equal(t, *app.InstallCommand, *unmarshaled.InstallCommand)
|
||||
assert.Equal(t, *app.LimitsCPUs, *unmarshaled.LimitsCPUs)
|
||||
assert.NotNil(t, unmarshaled.Settings)
|
||||
assert.True(t, *unmarshaled.Settings.IsAutoDeployEnabled)
|
||||
}
|
||||
|
||||
func TestApplication_UnmarshalFromFixture(t *testing.T) {
|
||||
fixtureData, err := os.ReadFile(filepath.Join("..", "..", "test", "fixtures", "application.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var app Application
|
||||
err = json.Unmarshal(fixtureData, &app)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "app-fixture-uuid-123", app.UUID)
|
||||
assert.Equal(t, "My Web Application", app.Name)
|
||||
assert.Equal(t, "running", app.Status)
|
||||
assert.Equal(t, "https://app.example.com", *app.FQDN)
|
||||
assert.Equal(t, "https://github.com/example/app", *app.GitRepository)
|
||||
assert.Equal(t, "main", *app.GitBranch)
|
||||
assert.Equal(t, "nixpacks", *app.BuildPack)
|
||||
assert.Equal(t, "3000", *app.PortsExposes)
|
||||
assert.Equal(t, "npm install", *app.InstallCommand)
|
||||
assert.Equal(t, "npm run build", *app.BuildCommand)
|
||||
assert.Equal(t, "npm start", *app.StartCommand)
|
||||
assert.Equal(t, "/health", *app.HealthCheckPath)
|
||||
assert.Equal(t, 200, *app.HealthCheckReturnCode)
|
||||
assert.Equal(t, "2", *app.LimitsCPUs)
|
||||
assert.Equal(t, "512M", *app.LimitsMemory)
|
||||
assert.Equal(t, "npm run migrate", *app.PreDeploymentCommand)
|
||||
assert.Equal(t, 1, *app.SwarmReplicas)
|
||||
assert.Equal(t, "abc123", *app.ConfigHash)
|
||||
|
||||
// Nested settings
|
||||
require.NotNil(t, app.Settings)
|
||||
assert.NotNil(t, app.Settings.IsAutoDeployEnabled)
|
||||
assert.True(t, *app.Settings.IsAutoDeployEnabled)
|
||||
assert.True(t, *app.Settings.IsForceHTTPSEnabled)
|
||||
assert.False(t, *app.Settings.IsStatic)
|
||||
}
|
||||
|
||||
func TestApplication_JSONExcludesHiddenFields(t *testing.T) {
|
||||
app := Application{
|
||||
ID: 42,
|
||||
UUID: "app-uuid",
|
||||
Name: "Test App",
|
||||
Status: "running",
|
||||
EnvironmentID: intPtr(1),
|
||||
DestinationID: intPtr(2),
|
||||
CreatedAt: "2024-01-01T00:00:00Z",
|
||||
UpdatedAt: "2024-01-02T00:00:00Z",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(app)
|
||||
require.NoError(t, err)
|
||||
|
||||
jsonStr := string(data)
|
||||
assert.NotContains(t, jsonStr, `"id"`)
|
||||
assert.NotContains(t, jsonStr, `"environment_id"`)
|
||||
assert.NotContains(t, jsonStr, `"destination_id"`)
|
||||
assert.NotContains(t, jsonStr, `"created_at"`)
|
||||
assert.NotContains(t, jsonStr, `"updated_at"`)
|
||||
assert.Contains(t, jsonStr, `"uuid"`)
|
||||
assert.Contains(t, jsonStr, `"name"`)
|
||||
}
|
||||
|
||||
func intPtr(v int) *int { return &v }
|
||||
|
||||
func TestResource_MarshalUnmarshal(t *testing.T) {
|
||||
resource := Resource{
|
||||
ID: 1,
|
||||
|
||||
@@ -82,6 +82,7 @@ type ServiceEnvironmentVariable struct {
|
||||
IsShownOnce bool `json:"is_shown_once"`
|
||||
IsRuntime bool `json:"is_runtime"`
|
||||
IsShared bool `json:"is_shared"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
RealValue *string `json:"real_value,omitempty" sensitive:"true"`
|
||||
ServiceID *int `json:"-" table:"-"`
|
||||
CreatedAt string `json:"-" table:"-"`
|
||||
@@ -90,12 +91,13 @@ type ServiceEnvironmentVariable struct {
|
||||
|
||||
// ServiceEnvironmentVariableCreateRequest represents the request to create a service environment variable
|
||||
type ServiceEnvironmentVariableCreateRequest struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
IsBuildTime *bool `json:"is_build_time,omitempty"`
|
||||
IsLiteral *bool `json:"is_literal,omitempty"`
|
||||
IsMultiline *bool `json:"is_multiline,omitempty"`
|
||||
IsRuntime *bool `json:"is_runtime,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
IsBuildTime *bool `json:"is_build_time,omitempty"`
|
||||
IsLiteral *bool `json:"is_literal,omitempty"`
|
||||
IsMultiline *bool `json:"is_multiline,omitempty"`
|
||||
IsRuntime *bool `json:"is_runtime,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceEnvironmentVariableUpdateRequest represents the request to update a service environment variable
|
||||
@@ -106,6 +108,7 @@ type ServiceEnvironmentVariableUpdateRequest struct {
|
||||
IsLiteral *bool `json:"is_literal,omitempty"`
|
||||
IsMultiline *bool `json:"is_multiline,omitempty"`
|
||||
IsRuntime *bool `json:"is_runtime,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceEnvBulkUpdateRequest represents the request to bulk update service environment variables
|
||||
|
||||
@@ -210,12 +210,23 @@ func TestTableFormatter_BooleanValues(t *testing.T) {
|
||||
|
||||
output := buf.String()
|
||||
|
||||
// Check boolean formatting
|
||||
lines := strings.Split(output, "\n")
|
||||
assert.Contains(t, lines[1], "true")
|
||||
assert.Contains(t, lines[1], "false")
|
||||
assert.Contains(t, lines[2], "false")
|
||||
assert.Contains(t, lines[2], "true")
|
||||
// Check boolean formatting - verify both rows contain expected values
|
||||
// Row 1: test1, true, false
|
||||
assert.Contains(t, output, "test1")
|
||||
assert.Contains(t, output, "test2")
|
||||
|
||||
// Find lines containing test data (not headers/borders)
|
||||
var dataLines []string
|
||||
for _, line := range strings.Split(output, "\n") {
|
||||
if strings.Contains(line, "test1") || strings.Contains(line, "test2") {
|
||||
dataLines = append(dataLines, line)
|
||||
}
|
||||
}
|
||||
require.Len(t, dataLines, 2, "expected exactly 2 data rows")
|
||||
assert.Contains(t, dataLines[0], "true")
|
||||
assert.Contains(t, dataLines[0], "false")
|
||||
assert.Contains(t, dataLines[1], "true")
|
||||
assert.Contains(t, dataLines[1], "false")
|
||||
}
|
||||
|
||||
func TestTableFormatter_NilPointer(t *testing.T) {
|
||||
|
||||
+34
-39
@@ -4,7 +4,9 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
)
|
||||
|
||||
// TableFormatter formats output as a table
|
||||
@@ -18,21 +20,6 @@ func NewTableFormatter(opts Options) *TableFormatter {
|
||||
}
|
||||
|
||||
func (f *TableFormatter) Format(data any) (err error) {
|
||||
w := tabwriter.NewWriter(f.opts.Writer, 0, 0, 2, ' ', tabwriter.Debug)
|
||||
defer func() {
|
||||
if flushErr := w.Flush(); flushErr != nil {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("failed to flush table writer: %w", flushErr)
|
||||
}
|
||||
}
|
||||
// Add a final newline nach table output, but only if no error occurred
|
||||
if err == nil {
|
||||
if _, nlErr := fmt.Fprintln(f.opts.Writer); nlErr != nil {
|
||||
err = fmt.Errorf("failed to write trailing newline: %w", nlErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle different data types
|
||||
val := reflect.ValueOf(data)
|
||||
|
||||
@@ -41,6 +28,24 @@ func (f *TableFormatter) Format(data any) (err error) {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
// Check for empty slice/array before creating the table writer
|
||||
if (val.Kind() == reflect.Slice || val.Kind() == reflect.Array) && val.Len() == 0 {
|
||||
if _, writeErr := fmt.Fprintln(f.opts.Writer, "No data"); writeErr != nil {
|
||||
return fmt.Errorf("failed to write no data message: %w", writeErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
w := tablewriter.NewWriter(f.opts.Writer)
|
||||
defer func() {
|
||||
if renderErr := w.Render(); renderErr != nil && err == nil {
|
||||
err = fmt.Errorf("failed to render table: %w", renderErr)
|
||||
}
|
||||
}()
|
||||
|
||||
// disable ALL CAPS for column headers
|
||||
w.Options(tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
|
||||
switch val.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
return f.formatSlice(w, val)
|
||||
@@ -54,14 +59,7 @@ func (f *TableFormatter) Format(data any) (err error) {
|
||||
}
|
||||
|
||||
// formatSlice formats a slice of structs as a table
|
||||
func (f *TableFormatter) formatSlice(w *tabwriter.Writer, val reflect.Value) error {
|
||||
if val.Len() == 0 {
|
||||
if _, err := fmt.Fprintln(w, "No data"); err != nil {
|
||||
return fmt.Errorf("failed to write no data message: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *TableFormatter) formatSlice(w *tablewriter.Table, val reflect.Value) error {
|
||||
// Get the first element to determine columns
|
||||
firstElem := val.Index(0)
|
||||
if firstElem.Kind() == reflect.Ptr {
|
||||
@@ -71,7 +69,9 @@ func (f *TableFormatter) formatSlice(w *tabwriter.Writer, val reflect.Value) err
|
||||
if firstElem.Kind() != reflect.Struct {
|
||||
// Simple slice (e.g., []string)
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
if _, err := fmt.Fprintf(w, "%v\n", val.Index(i).Interface()); err != nil {
|
||||
elem := val.Index(i)
|
||||
row := []string{f.formatValue(elem)}
|
||||
if err := w.Append(row); err != nil {
|
||||
return fmt.Errorf("failed to write slice element: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -82,9 +82,7 @@ func (f *TableFormatter) formatSlice(w *tabwriter.Writer, val reflect.Value) err
|
||||
headers := f.getHeaders(firstElem.Type())
|
||||
// Add # as first column header
|
||||
headersWithNum := append([]string{"#"}, headers...)
|
||||
if _, err := fmt.Fprintln(w, strings.Join(headersWithNum, "\t")); err != nil {
|
||||
return fmt.Errorf("failed to write table headers: %w", err)
|
||||
}
|
||||
w.Header(headersWithNum)
|
||||
|
||||
// Print rows
|
||||
for i := 0; i < val.Len(); i++ {
|
||||
@@ -95,7 +93,7 @@ func (f *TableFormatter) formatSlice(w *tabwriter.Writer, val reflect.Value) err
|
||||
row := f.formatStructRow(elem)
|
||||
// Add row number (1-indexed) as first column
|
||||
rowWithNum := append([]string{fmt.Sprintf("%d", i+1)}, row...)
|
||||
if _, err := fmt.Fprintln(w, strings.Join(rowWithNum, "\t")); err != nil {
|
||||
if err := w.Append(rowWithNum); err != nil {
|
||||
return fmt.Errorf("failed to write table row: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -104,16 +102,14 @@ func (f *TableFormatter) formatSlice(w *tabwriter.Writer, val reflect.Value) err
|
||||
}
|
||||
|
||||
// formatStruct formats a single struct as a table (horizontal layout with headers)
|
||||
func (f *TableFormatter) formatStruct(w *tabwriter.Writer, val reflect.Value) error {
|
||||
func (f *TableFormatter) formatStruct(w *tablewriter.Table, val reflect.Value) error {
|
||||
// Get headers
|
||||
headers := f.getHeaders(val.Type())
|
||||
if _, err := fmt.Fprintln(w, strings.Join(headers, "\t")); err != nil {
|
||||
return fmt.Errorf("failed to write struct headers: %w", err)
|
||||
}
|
||||
w.Header(headers)
|
||||
|
||||
// Get row data
|
||||
row := f.formatStructRow(val)
|
||||
if _, err := fmt.Fprintln(w, strings.Join(row, "\t")); err != nil {
|
||||
if err := w.Append(row); err != nil {
|
||||
return fmt.Errorf("failed to write struct row: %w", err)
|
||||
}
|
||||
|
||||
@@ -121,16 +117,15 @@ func (f *TableFormatter) formatStruct(w *tabwriter.Writer, val reflect.Value) er
|
||||
}
|
||||
|
||||
// formatMap formats a map as a table
|
||||
func (f *TableFormatter) formatMap(w *tabwriter.Writer, val reflect.Value) error {
|
||||
if _, err := fmt.Fprintln(w, "Key\tValue"); err != nil {
|
||||
return fmt.Errorf("failed to write map headers: %w", err)
|
||||
}
|
||||
func (f *TableFormatter) formatMap(w *tablewriter.Table, val reflect.Value) error {
|
||||
w.Header([]string{"Key", "Value"})
|
||||
|
||||
iter := val.MapRange()
|
||||
for iter.Next() {
|
||||
key := iter.Key()
|
||||
value := iter.Value()
|
||||
if _, err := fmt.Fprintf(w, "%v\t%v\n", key.Interface(), f.formatValue(value)); err != nil {
|
||||
row := []string{f.formatValue(key), f.formatValue(value)}
|
||||
if err := w.Append(row); err != nil {
|
||||
return fmt.Errorf("failed to write map entry: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,9 +184,7 @@ type BulkUpdateEnvsRequest struct {
|
||||
}
|
||||
|
||||
// BulkUpdateEnvsResponse represents the response from bulk update
|
||||
type BulkUpdateEnvsResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
type BulkUpdateEnvsResponse []models.EnvironmentVariable
|
||||
|
||||
// BulkUpdateEnvs updates multiple environment variables in a single request
|
||||
func (s *ApplicationService) BulkUpdateEnvs(ctx context.Context, appUUID string, req *BulkUpdateEnvsRequest) (*BulkUpdateEnvsResponse, error) {
|
||||
@@ -198,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
|
||||
|
||||
@@ -110,17 +110,41 @@ func TestApplicationService_Get(t *testing.T) {
|
||||
desc := "Test Application"
|
||||
branch := "main"
|
||||
fqdn := "test.example.com"
|
||||
repo := "https://github.com/example/app"
|
||||
buildPack := "nixpacks"
|
||||
ports := "3000"
|
||||
installCmd := "npm install"
|
||||
buildCmd := "npm run build"
|
||||
startCmd := "npm start"
|
||||
healthPath := "/health"
|
||||
limitsCPUs := "2"
|
||||
limitsMemory := "512M"
|
||||
preDeployCmd := "npm run migrate"
|
||||
isAutoDeployEnabled := true
|
||||
|
||||
application := models.Application{
|
||||
ID: 1,
|
||||
UUID: "app-uuid-123",
|
||||
Name: "Test App",
|
||||
Description: &desc,
|
||||
Status: "running",
|
||||
GitBranch: &branch,
|
||||
FQDN: &fqdn,
|
||||
CreatedAt: "2024-01-01T00:00:00Z",
|
||||
UpdatedAt: "2024-01-02T00:00:00Z",
|
||||
ID: 1,
|
||||
UUID: "app-uuid-123",
|
||||
Name: "Test App",
|
||||
Description: &desc,
|
||||
Status: "running",
|
||||
GitBranch: &branch,
|
||||
FQDN: &fqdn,
|
||||
GitRepository: &repo,
|
||||
BuildPack: &buildPack,
|
||||
PortsExposes: &ports,
|
||||
InstallCommand: &installCmd,
|
||||
BuildCommand: &buildCmd,
|
||||
StartCommand: &startCmd,
|
||||
HealthCheckPath: &healthPath,
|
||||
LimitsCPUs: &limitsCPUs,
|
||||
LimitsMemory: &limitsMemory,
|
||||
PreDeploymentCommand: &preDeployCmd,
|
||||
Settings: &models.ApplicationSettings{
|
||||
IsAutoDeployEnabled: &isAutoDeployEnabled,
|
||||
},
|
||||
CreatedAt: "2024-01-01T00:00:00Z",
|
||||
UpdatedAt: "2024-01-02T00:00:00Z",
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -144,6 +168,18 @@ func TestApplicationService_Get(t *testing.T) {
|
||||
assert.Equal(t, "running", result.Status)
|
||||
assert.Equal(t, "main", *result.GitBranch)
|
||||
assert.Equal(t, "test.example.com", *result.FQDN)
|
||||
assert.Equal(t, "https://github.com/example/app", *result.GitRepository)
|
||||
assert.Equal(t, "nixpacks", *result.BuildPack)
|
||||
assert.Equal(t, "3000", *result.PortsExposes)
|
||||
assert.Equal(t, "npm install", *result.InstallCommand)
|
||||
assert.Equal(t, "npm run build", *result.BuildCommand)
|
||||
assert.Equal(t, "npm start", *result.StartCommand)
|
||||
assert.Equal(t, "/health", *result.HealthCheckPath)
|
||||
assert.Equal(t, "2", *result.LimitsCPUs)
|
||||
assert.Equal(t, "512M", *result.LimitsMemory)
|
||||
assert.Equal(t, "npm run migrate", *result.PreDeploymentCommand)
|
||||
require.NotNil(t, result.Settings)
|
||||
assert.True(t, *result.Settings.IsAutoDeployEnabled)
|
||||
}
|
||||
|
||||
func TestApplicationService_Get_NotFound(t *testing.T) {
|
||||
@@ -724,10 +760,12 @@ func TestApplicationService_UpdateEnv(t *testing.T) {
|
||||
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs", r.URL.Path)
|
||||
assert.Equal(t, "PATCH", r.Method)
|
||||
|
||||
comment := "API key for external service"
|
||||
env := models.EnvironmentVariable{
|
||||
UUID: "env-uuid-1",
|
||||
Key: "API_KEY",
|
||||
Value: "newsecret456",
|
||||
UUID: "env-uuid-1",
|
||||
Key: "API_KEY",
|
||||
Value: "newsecret456",
|
||||
Comment: &comment,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(env)
|
||||
@@ -739,9 +777,11 @@ func TestApplicationService_UpdateEnv(t *testing.T) {
|
||||
|
||||
newKey := "API_KEY"
|
||||
newValue := "newsecret456"
|
||||
newComment := "API key for external service"
|
||||
req := &models.EnvironmentVariableUpdateRequest{
|
||||
Key: &newKey,
|
||||
Value: &newValue,
|
||||
Key: &newKey,
|
||||
Value: &newValue,
|
||||
Comment: &newComment,
|
||||
}
|
||||
|
||||
result, err := svc.UpdateEnv(context.Background(), "app-uuid-123", req)
|
||||
@@ -749,6 +789,7 @@ func TestApplicationService_UpdateEnv(t *testing.T) {
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "API_KEY", result.Key)
|
||||
assert.Equal(t, "newsecret456", result.Value)
|
||||
assert.Equal(t, "API key for external service", *result.Comment)
|
||||
}
|
||||
|
||||
func TestApplicationService_UpdateEnv_Error(t *testing.T) {
|
||||
@@ -1196,3 +1237,328 @@ func TestApplicationService_CreateDockerImage(t *testing.T) {
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "new-app-uuid", result.UUID)
|
||||
}
|
||||
|
||||
func TestApplicationService_BulkUpdateEnvs(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs/bulk", r.URL.Path)
|
||||
assert.Equal(t, "PATCH", r.Method)
|
||||
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
|
||||
|
||||
// Verify request body was correctly serialized
|
||||
var reqBody BulkUpdateEnvsRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&reqBody)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reqBody.Data, 1)
|
||||
assert.Equal(t, "KEY1", reqBody.Data[0].Key)
|
||||
assert.Equal(t, "VAL1", reqBody.Data[0].Value)
|
||||
|
||||
// Return array response as per API
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
resp := []models.EnvironmentVariable{
|
||||
{
|
||||
Key: "KEY1",
|
||||
Value: "VAL1",
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
req := &BulkUpdateEnvsRequest{
|
||||
Data: []models.EnvironmentVariableCreateRequest{
|
||||
{Key: "KEY1", Value: "VAL1"},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := svc.BulkUpdateEnvs(context.Background(), "app-uuid-123", req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Len(t, *result, 1)
|
||||
assert.Equal(t, "KEY1", (*result)[0].Key)
|
||||
}
|
||||
|
||||
func TestApplicationService_BulkUpdateEnvs_APIError(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)
|
||||
|
||||
req := &BulkUpdateEnvsRequest{
|
||||
Data: []models.EnvironmentVariableCreateRequest{
|
||||
{Key: "KEY1", Value: "VAL1"},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := svc.BulkUpdateEnvs(context.Background(), "app-uuid-123", req)
|
||||
require.Error(t, err)
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -173,6 +173,108 @@ func (s *DatabaseService) DeleteBackupExecution(ctx context.Context, dbUUID, bac
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListEnvs retrieves all environment variables for a database
|
||||
func (s *DatabaseService) ListEnvs(ctx context.Context, uuid string) ([]models.DatabaseEnvironmentVariable, error) {
|
||||
var envs []models.DatabaseEnvironmentVariable
|
||||
err := s.client.Get(ctx, fmt.Sprintf("databases/%s/envs", uuid), &envs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list environment variables for database %s: %w", uuid, err)
|
||||
}
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
// GetEnv retrieves a single environment variable by UUID or key
|
||||
func (s *DatabaseService) GetEnv(ctx context.Context, dbUUID, envIdentifier string) (*models.DatabaseEnvironmentVariable, error) {
|
||||
envs, err := s.ListEnvs(ctx, dbUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, env := range envs {
|
||||
if env.UUID == envIdentifier || env.Key == envIdentifier {
|
||||
return &env, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("environment variable '%s' not found in database %s", envIdentifier, dbUUID)
|
||||
}
|
||||
|
||||
// CreateEnv creates a new environment variable for a database
|
||||
func (s *DatabaseService) CreateEnv(ctx context.Context, uuid string, req *models.DatabaseEnvironmentVariableCreateRequest) (*models.DatabaseEnvironmentVariable, error) {
|
||||
var env models.DatabaseEnvironmentVariable
|
||||
err := s.client.Post(ctx, fmt.Sprintf("databases/%s/envs", uuid), req, &env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create environment variable for database %s: %w", uuid, err)
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
|
||||
// UpdateEnv updates an environment variable for a database
|
||||
func (s *DatabaseService) UpdateEnv(ctx context.Context, dbUUID string, req *models.DatabaseEnvironmentVariableUpdateRequest) (*models.DatabaseEnvironmentVariable, error) {
|
||||
var env models.DatabaseEnvironmentVariable
|
||||
err := s.client.Patch(ctx, fmt.Sprintf("databases/%s/envs", dbUUID), req, &env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update environment variable for database %s: %w", dbUUID, err)
|
||||
}
|
||||
return &env, nil
|
||||
}
|
||||
|
||||
// DeleteEnv deletes an environment variable from a database
|
||||
func (s *DatabaseService) DeleteEnv(ctx context.Context, dbUUID, envUUID string) error {
|
||||
err := s.client.Delete(ctx, fmt.Sprintf("databases/%s/envs/%s", dbUUID, envUUID))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete environment variable %s from database %s: %w", envUUID, dbUUID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BulkUpdateEnvs updates multiple environment variables for a database in a single request
|
||||
func (s *DatabaseService) BulkUpdateEnvs(ctx context.Context, dbUUID string, req *models.DatabaseEnvBulkUpdateRequest) (models.DatabaseEnvBulkUpdateResponse, error) {
|
||||
var response models.DatabaseEnvBulkUpdateResponse
|
||||
err := s.client.Patch(ctx, fmt.Sprintf("databases/%s/envs/bulk", dbUUID), req, &response)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to bulk update environment variables for database %s: %w", dbUUID, err)
|
||||
}
|
||||
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
|
||||
|
||||
@@ -848,6 +848,252 @@ func TestDatabaseService_DeleteBackupExecution(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Environment Variable Tests ---
|
||||
|
||||
func TestDatabaseService_ListEnvs(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/databases/db-uuid-123/envs", r.URL.Path)
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`[
|
||||
{"uuid": "env-1", "key": "DB_HOST", "value": "localhost", "is_literal": false},
|
||||
{"uuid": "env-2", "key": "DB_PORT", "value": "5432", "is_literal": true}
|
||||
]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDatabaseService(client)
|
||||
|
||||
envs, err := svc.ListEnvs(context.Background(), "db-uuid-123")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, envs, 2)
|
||||
assert.Equal(t, "DB_HOST", envs[0].Key)
|
||||
assert.Equal(t, "DB_PORT", envs[1].Key)
|
||||
}
|
||||
|
||||
func TestDatabaseService_ListEnvs_Error(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"message":"database not found"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDatabaseService(client)
|
||||
|
||||
envs, err := svc.ListEnvs(context.Background(), "db-uuid-123")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, envs)
|
||||
assert.Contains(t, err.Error(), "failed to list environment variables")
|
||||
}
|
||||
|
||||
func TestDatabaseService_GetEnv(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/databases/db-uuid-123/envs", r.URL.Path)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`[
|
||||
{"uuid": "env-1", "key": "DB_HOST", "value": "localhost"},
|
||||
{"uuid": "env-2", "key": "DB_PORT", "value": "5432"}
|
||||
]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDatabaseService(client)
|
||||
|
||||
// Find by UUID
|
||||
env, err := svc.GetEnv(context.Background(), "db-uuid-123", "env-2")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "DB_PORT", env.Key)
|
||||
|
||||
// Find by key
|
||||
env, err = svc.GetEnv(context.Background(), "db-uuid-123", "DB_HOST")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "env-1", env.UUID)
|
||||
|
||||
// Not found
|
||||
env, err = svc.GetEnv(context.Background(), "db-uuid-123", "NONEXISTENT")
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, env)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
func TestDatabaseService_CreateEnv(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/databases/db-uuid-123/envs", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
var req models.DatabaseEnvironmentVariableCreateRequest
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
assert.Equal(t, "NEW_VAR", req.Key)
|
||||
assert.Equal(t, "new_value", req.Value)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"uuid": "env-new",
|
||||
"key": "NEW_VAR",
|
||||
"value": "new_value",
|
||||
"comment": "test comment"
|
||||
}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDatabaseService(client)
|
||||
|
||||
comment := "test comment"
|
||||
env, err := svc.CreateEnv(context.Background(), "db-uuid-123", &models.DatabaseEnvironmentVariableCreateRequest{
|
||||
Key: "NEW_VAR",
|
||||
Value: "new_value",
|
||||
Comment: &comment,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "NEW_VAR", env.Key)
|
||||
assert.Equal(t, "new_value", env.Value)
|
||||
assert.Equal(t, "test comment", *env.Comment)
|
||||
}
|
||||
|
||||
func TestDatabaseService_CreateEnv_Error(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
_, _ = w.Write([]byte(`{"message":"validation failed"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDatabaseService(client)
|
||||
|
||||
env, err := svc.CreateEnv(context.Background(), "db-uuid-123", &models.DatabaseEnvironmentVariableCreateRequest{
|
||||
Key: "NEW_VAR",
|
||||
Value: "new_value",
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, env)
|
||||
assert.Contains(t, err.Error(), "failed to create environment variable")
|
||||
}
|
||||
|
||||
func TestDatabaseService_UpdateEnv(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/databases/db-uuid-123/envs", r.URL.Path)
|
||||
assert.Equal(t, "PATCH", r.Method)
|
||||
|
||||
comment := "updated comment"
|
||||
env := models.DatabaseEnvironmentVariable{
|
||||
UUID: "env-1",
|
||||
Key: "DB_HOST",
|
||||
Value: "newhost",
|
||||
Comment: &comment,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(env)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDatabaseService(client)
|
||||
|
||||
newKey := "DB_HOST"
|
||||
newValue := "newhost"
|
||||
newComment := "updated comment"
|
||||
req := &models.DatabaseEnvironmentVariableUpdateRequest{
|
||||
Key: &newKey,
|
||||
Value: &newValue,
|
||||
Comment: &newComment,
|
||||
}
|
||||
|
||||
result, err := svc.UpdateEnv(context.Background(), "db-uuid-123", req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "DB_HOST", result.Key)
|
||||
assert.Equal(t, "newhost", result.Value)
|
||||
assert.Equal(t, "updated comment", *result.Comment)
|
||||
}
|
||||
|
||||
func TestDatabaseService_UpdateEnv_Error(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"message":"environment variable not found"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDatabaseService(client)
|
||||
|
||||
newKey := "DB_HOST"
|
||||
newValue := "newhost"
|
||||
result, err := svc.UpdateEnv(context.Background(), "db-uuid-123", &models.DatabaseEnvironmentVariableUpdateRequest{
|
||||
Key: &newKey,
|
||||
Value: &newValue,
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "failed to update environment variable")
|
||||
}
|
||||
|
||||
func TestDatabaseService_DeleteEnv(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/databases/db-uuid-123/envs/env-1", r.URL.Path)
|
||||
assert.Equal(t, "DELETE", r.Method)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDatabaseService(client)
|
||||
|
||||
err := svc.DeleteEnv(context.Background(), "db-uuid-123", "env-1")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDatabaseService_DeleteEnv_Error(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"message":"not found"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDatabaseService(client)
|
||||
|
||||
err := svc.DeleteEnv(context.Background(), "db-uuid-123", "env-1")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to delete environment variable")
|
||||
}
|
||||
|
||||
func TestDatabaseService_BulkUpdateEnvs(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/databases/db-uuid-123/envs/bulk", r.URL.Path)
|
||||
assert.Equal(t, "PATCH", r.Method)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`[
|
||||
{"uuid": "env-1", "key": "DB_HOST", "value": "localhost"},
|
||||
{"uuid": "env-2", "key": "DB_PORT", "value": "5432"}
|
||||
]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDatabaseService(client)
|
||||
|
||||
req := &models.DatabaseEnvBulkUpdateRequest{
|
||||
Data: []models.DatabaseEnvironmentVariableCreateRequest{
|
||||
{Key: "DB_HOST", Value: "localhost"},
|
||||
{Key: "DB_PORT", Value: "5432"},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := svc.BulkUpdateEnvs(context.Background(), "db-uuid-123", req)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result, 2)
|
||||
}
|
||||
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
@@ -855,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)
|
||||
}
|
||||
|
||||
@@ -35,16 +35,11 @@ type DeployResponse struct {
|
||||
}
|
||||
|
||||
// Deploy triggers a deployment for a resource
|
||||
func (s *DeploymentService) Deploy(ctx context.Context, uuid string, force bool) (*DeployResponse, error) {
|
||||
endpoint := fmt.Sprintf("deploy?uuid=%s", uuid)
|
||||
if force {
|
||||
endpoint += "&force=true"
|
||||
}
|
||||
|
||||
func (s *DeploymentService) Deploy(ctx context.Context, req models.DeployRequest) (*DeployResponse, error) {
|
||||
var response DeployResponse
|
||||
err := s.client.Get(ctx, endpoint, &response)
|
||||
err := s.client.Post(ctx, "deploy", req, &response)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to deploy resource %s: %w", uuid, err)
|
||||
return nil, fmt.Errorf("failed to deploy resource %s: %w", req.UUID, err)
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -11,21 +12,24 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/api"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
)
|
||||
|
||||
func TestDeploymentService_Deploy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
uuid string
|
||||
force bool
|
||||
expectedPath string
|
||||
response DeployResponse
|
||||
name string
|
||||
request models.DeployRequest
|
||||
expectedPath string
|
||||
expectedMethod string
|
||||
expectedBody string
|
||||
response DeployResponse
|
||||
}{
|
||||
{
|
||||
name: "deploy without force",
|
||||
uuid: "res-123",
|
||||
force: false,
|
||||
expectedPath: "/api/v1/deploy?uuid=res-123",
|
||||
name: "deploy without optional fields",
|
||||
request: models.DeployRequest{UUID: "res-123"},
|
||||
expectedPath: "/api/v1/deploy",
|
||||
expectedMethod: "POST",
|
||||
expectedBody: `{"uuid":"res-123"}`,
|
||||
response: DeployResponse{
|
||||
Deployments: []DeploymentInfo{
|
||||
{
|
||||
@@ -37,10 +41,16 @@ func TestDeploymentService_Deploy(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deploy with force",
|
||||
uuid: "res-789",
|
||||
force: true,
|
||||
expectedPath: "/api/v1/deploy?uuid=res-789&force=true",
|
||||
name: "deploy with force and extra payload fields",
|
||||
request: models.DeployRequest{
|
||||
UUID: "res-789",
|
||||
Force: deployBoolPtr(true),
|
||||
PullRequestID: deployIntPtr(2345),
|
||||
DockerTag: deployStringPtr("1.28.3"),
|
||||
},
|
||||
expectedPath: "/api/v1/deploy",
|
||||
expectedMethod: "POST",
|
||||
expectedBody: `{"uuid":"res-789","force":true,"pull_request_id":2345,"docker_tag":"1.28.3"}`,
|
||||
response: DeployResponse{
|
||||
Deployments: []DeploymentInfo{
|
||||
{
|
||||
@@ -56,8 +66,12 @@ func TestDeploymentService_Deploy(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, tt.expectedPath, r.URL.Path+"?"+r.URL.RawQuery)
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
assert.Equal(t, tt.expectedPath, r.URL.Path)
|
||||
assert.Equal(t, tt.expectedMethod, r.Method)
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.JSONEq(t, tt.expectedBody, string(body))
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(tt.response)
|
||||
@@ -67,7 +81,7 @@ func TestDeploymentService_Deploy(t *testing.T) {
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewDeploymentService(client)
|
||||
|
||||
result, err := svc.Deploy(context.Background(), tt.uuid, tt.force)
|
||||
result, err := svc.Deploy(context.Background(), tt.request)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result.Deployments, len(tt.response.Deployments))
|
||||
if len(result.Deployments) > 0 {
|
||||
@@ -78,6 +92,18 @@ func TestDeploymentService_Deploy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func deployBoolPtr(v bool) *bool {
|
||||
return &v
|
||||
}
|
||||
|
||||
func deployIntPtr(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
func deployStringPtr(v string) *string {
|
||||
return &v
|
||||
}
|
||||
|
||||
func TestDeploymentService_ListByApplication(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -320,7 +321,8 @@ func TestService_UpdateEnv(t *testing.T) {
|
||||
"key": "UPDATED_VAR",
|
||||
"value": "updated_value",
|
||||
"is_buildtime": true,
|
||||
"is_preview": false
|
||||
"is_preview": false,
|
||||
"comment": "updated comment"
|
||||
}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
@@ -329,12 +331,15 @@ func TestService_UpdateEnv(t *testing.T) {
|
||||
svc := NewService(client)
|
||||
|
||||
newKey := "UPDATED_VAR"
|
||||
newComment := "updated comment"
|
||||
env, err := svc.UpdateEnv(context.Background(), "service-uuid-123", &models.ServiceEnvironmentVariableUpdateRequest{
|
||||
Key: &newKey,
|
||||
Key: &newKey,
|
||||
Comment: &newComment,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "UPDATED_VAR", env.Key)
|
||||
assert.Equal(t, "updated comment", *env.Comment)
|
||||
}
|
||||
|
||||
func TestService_DeleteEnv(t *testing.T) {
|
||||
@@ -435,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)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
@@ -14,7 +15,7 @@ import (
|
||||
|
||||
// Version variables injected by GoReleaser at build time via ldflags
|
||||
var (
|
||||
version = "v1.4.0"
|
||||
version = "v1.6.0"
|
||||
)
|
||||
|
||||
// GitHubAPIURL is the URL for fetching CLI version tags (exported for testing)
|
||||
@@ -101,7 +102,7 @@ func CheckLatestVersionOfCli(_ bool) (string, error) {
|
||||
}
|
||||
|
||||
if latestVersion.GreaterThan(currentVersion) {
|
||||
fmt.Printf("A new version (%s) is available. Update with: coolify update\n", latestVersion.String())
|
||||
_, _ = fmt.Fprintf(os.Stderr, "A new version (%s) is available. Update with: coolify update\n", latestVersion.String())
|
||||
}
|
||||
return latestVersion.String(), nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,39 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func captureOutput(t *testing.T, fn func()) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
|
||||
stdoutR, stdoutW, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("os.Pipe() stdout error = %v", err)
|
||||
}
|
||||
stderrR, stderrW, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("os.Pipe() stderr error = %v", err)
|
||||
}
|
||||
|
||||
os.Stdout = stdoutW
|
||||
os.Stderr = stderrW
|
||||
|
||||
fn()
|
||||
|
||||
_ = stdoutW.Close()
|
||||
_ = stderrW.Close()
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
|
||||
var stdoutBuf bytes.Buffer
|
||||
var stderrBuf bytes.Buffer
|
||||
_, _ = io.Copy(&stdoutBuf, stdoutR)
|
||||
_, _ = io.Copy(&stderrBuf, stderrR)
|
||||
|
||||
return stdoutBuf.String(), stderrBuf.String()
|
||||
}
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
v := GetVersion()
|
||||
if v == "" {
|
||||
@@ -42,19 +75,11 @@ func TestCheckLatestVersionOfCli_UpdateAvailable(t *testing.T) {
|
||||
|
||||
GitHubAPIURL = server.URL
|
||||
|
||||
// Capture stdout to check for update message
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
latestVersion, err := CheckLatestVersionOfCli(false)
|
||||
|
||||
_ = w.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
var latestVersion string
|
||||
var err error
|
||||
stdout, stderr := captureOutput(t, func() {
|
||||
latestVersion, err = CheckLatestVersionOfCli(false)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err)
|
||||
@@ -64,10 +89,15 @@ func TestCheckLatestVersionOfCli_UpdateAvailable(t *testing.T) {
|
||||
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want %q", latestVersion, "2.0.0")
|
||||
}
|
||||
|
||||
// Should print update message
|
||||
// Should not write anything to stdout
|
||||
if stdout != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() stdout = %q, want empty string", stdout)
|
||||
}
|
||||
|
||||
// Should print update message to stderr
|
||||
expectedMsg := "A new version (2.0.0) is available. Update with: coolify update\n"
|
||||
if output != expectedMsg {
|
||||
t.Errorf("CheckLatestVersionOfCli() output = %q, want %q", output, expectedMsg)
|
||||
if stderr != expectedMsg {
|
||||
t.Errorf("CheckLatestVersionOfCli() stderr = %q, want %q", stderr, expectedMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,19 +122,11 @@ func TestCheckLatestVersionOfCli_NoUpdate(t *testing.T) {
|
||||
|
||||
GitHubAPIURL = server.URL
|
||||
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
latestVersion, err := CheckLatestVersionOfCli(false)
|
||||
|
||||
_ = w.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
var latestVersion string
|
||||
var err error
|
||||
stdout, stderr := captureOutput(t, func() {
|
||||
latestVersion, err = CheckLatestVersionOfCli(false)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err)
|
||||
@@ -116,8 +138,12 @@ func TestCheckLatestVersionOfCli_NoUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
// Should NOT print any message when already on latest (current v99.99.99 > latest v2.0.0)
|
||||
if output != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not print anything when on latest version, got: %q", output)
|
||||
if stdout != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not write to stdout when on latest version, got: %q", stdout)
|
||||
}
|
||||
|
||||
if stderr != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not write to stderr when on latest version, got: %q", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,19 +163,11 @@ func TestCheckLatestVersionOfCli_APIError_SilentFail(t *testing.T) {
|
||||
|
||||
GitHubAPIURL = server.URL
|
||||
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
latestVersion, err := CheckLatestVersionOfCli(false)
|
||||
|
||||
_ = w.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
var latestVersion string
|
||||
var err error
|
||||
stdout, stderr := captureOutput(t, func() {
|
||||
latestVersion, err = CheckLatestVersionOfCli(false)
|
||||
})
|
||||
|
||||
// Should return empty string and nil error (silent fail)
|
||||
if err != nil {
|
||||
@@ -161,8 +179,12 @@ func TestCheckLatestVersionOfCli_APIError_SilentFail(t *testing.T) {
|
||||
}
|
||||
|
||||
// Should NOT print anything on error
|
||||
if output != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not print anything on API error, got: %q", output)
|
||||
if stdout != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not print anything to stdout on API error, got: %q", stdout)
|
||||
}
|
||||
|
||||
if stderr != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not print anything to stderr on API error, got: %q", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,19 +198,11 @@ func TestCheckLatestVersionOfCli_NetworkError_SilentFail(t *testing.T) {
|
||||
// Use invalid URL to cause network error
|
||||
GitHubAPIURL = "http://localhost:1" // Port 1 should fail to connect
|
||||
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
latestVersion, err := CheckLatestVersionOfCli(false)
|
||||
|
||||
_ = w.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
var buf bytes.Buffer
|
||||
_, _ = io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
var latestVersion string
|
||||
var err error
|
||||
stdout, stderr := captureOutput(t, func() {
|
||||
latestVersion, err = CheckLatestVersionOfCli(false)
|
||||
})
|
||||
|
||||
// Should return empty string and nil error (silent fail)
|
||||
if err != nil {
|
||||
@@ -200,8 +214,46 @@ func TestCheckLatestVersionOfCli_NetworkError_SilentFail(t *testing.T) {
|
||||
}
|
||||
|
||||
// Should NOT print anything on error
|
||||
if output != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not print anything on network error, got: %q", output)
|
||||
if stdout != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not print anything to stdout on network error, got: %q", stdout)
|
||||
}
|
||||
|
||||
if stderr != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not print anything to stderr on network error, got: %q", stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLatestVersionOfCli_UpdateAvailable_LeavesStdoutAvailableForJSON(t *testing.T) {
|
||||
originalURL := GitHubAPIURL
|
||||
originalVersion := version
|
||||
defer func() {
|
||||
GitHubAPIURL = originalURL
|
||||
version = originalVersion
|
||||
}()
|
||||
|
||||
version = "v0.0.1"
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`[{"ref":"refs/tags/v2.0.0"}]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
GitHubAPIURL = server.URL
|
||||
|
||||
stdout, stderr := captureOutput(t, func() {
|
||||
_, _ = CheckLatestVersionOfCli(false)
|
||||
_, _ = os.Stdout.WriteString(`[{"uuid":"demo"}]` + "\n")
|
||||
})
|
||||
|
||||
expectedStdout := `[{"uuid":"demo"}]` + "\n"
|
||||
if stdout != expectedStdout {
|
||||
t.Fatalf("stdout = %q, want %q", stdout, expectedStdout)
|
||||
}
|
||||
|
||||
expectedStderr := "A new version (2.0.0) is available. Update with: coolify update\n"
|
||||
if stderr != expectedStderr {
|
||||
t.Fatalf("stderr = %q, want %q", stderr, expectedStderr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2255
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,133 @@
|
||||
# Coolify CLI - llms.txt
|
||||
|
||||
> Quick AI/LLM instructions for the Coolify CLI.
|
||||
> Source: https://github.com/coollabsio/coolify-cli
|
||||
> API Spec: https://github.com/coollabsio/coolify/blob/v4.x/openapi.json
|
||||
|
||||
## Operating Rules
|
||||
|
||||
- Prefer `--format json` for automation and parsing.
|
||||
- Use Coolify UUIDs for resources; do not use internal numeric IDs.
|
||||
- Team commands are the exception: they use numeric team IDs.
|
||||
- Authenticate with a saved context when possible; use `--token` only for overrides.
|
||||
- Use `llms-full.txt` for the exhaustive command/flag catalog.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Linux/macOS (recommended)
|
||||
curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.sh | bash
|
||||
|
||||
# Homebrew (macOS/Linux)
|
||||
brew install coollabsio/coolify-cli/coolify-cli
|
||||
|
||||
# Windows (PowerShell)
|
||||
irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
|
||||
|
||||
# Go install
|
||||
go install github.com/coollabsio/coolify-cli/coolify@latest
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
1. Get an API token from your Coolify dashboard at `/security/api-tokens`
|
||||
2. For Coolify Cloud: `coolify context set-token cloud <token>`
|
||||
3. For self-hosted: `coolify context add -d <context_name> <url> <token>`
|
||||
4. Switch contexts with `coolify context use <context_name>`
|
||||
|
||||
## Configuration
|
||||
|
||||
Config file location:
|
||||
- Linux/macOS: `~/.config/coolify/config.json`
|
||||
- Windows: `%APPDATA%\coolify\config.json`
|
||||
|
||||
Supports multiple contexts (instances) with `coolify context` commands.
|
||||
|
||||
## Output Formats
|
||||
|
||||
All commands support `--format` flag:
|
||||
- `table` (default) - human-readable tabular output
|
||||
- `json` - compact JSON for scripting
|
||||
- `pretty` - indented JSON for debugging
|
||||
|
||||
## Global Flags
|
||||
|
||||
- `--context <name>` - use a specific saved context
|
||||
- `--token <token>` - override token from config
|
||||
- `--format table|json|pretty` - choose output format
|
||||
- `--show-sensitive` - reveal sensitive values
|
||||
- `--debug` - enable debug output
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Contexts
|
||||
|
||||
```bash
|
||||
coolify context list
|
||||
coolify context verify
|
||||
coolify context version
|
||||
coolify context use prod
|
||||
```
|
||||
|
||||
### Inventory
|
||||
|
||||
```bash
|
||||
coolify server list
|
||||
coolify project list
|
||||
coolify resource list
|
||||
coolify app list
|
||||
coolify service list
|
||||
coolify database list
|
||||
```
|
||||
|
||||
### Applications
|
||||
|
||||
```bash
|
||||
coolify app get <uuid>
|
||||
coolify app start <uuid>
|
||||
coolify app stop <uuid>
|
||||
coolify app restart <uuid>
|
||||
coolify app logs <uuid> --follow
|
||||
coolify app deployments list <app-uuid>
|
||||
coolify app deployments logs <app-uuid> --follow
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
coolify app env list <app-uuid>
|
||||
coolify app env create <app-uuid> --key API_KEY --value secret123
|
||||
coolify app env update <app-uuid> <env-uuid-or-key> --value new-secret
|
||||
coolify app env sync <app-uuid> --file .env.production --build-time --preview
|
||||
```
|
||||
|
||||
### Deployments
|
||||
|
||||
```bash
|
||||
coolify deploy list
|
||||
coolify deploy name my-application
|
||||
coolify deploy batch api,worker,frontend --force
|
||||
coolify deploy cancel <deployment-uuid>
|
||||
```
|
||||
|
||||
### Databases and Services
|
||||
|
||||
```bash
|
||||
coolify database get <uuid>
|
||||
coolify database create postgresql --server-uuid <uuid> --project-uuid <uuid> --environment-name production
|
||||
coolify database backup list <database-uuid>
|
||||
coolify service get <uuid>
|
||||
coolify service create <type> --project-uuid <uuid> --server-uuid <uuid> --instant-deploy
|
||||
```
|
||||
|
||||
## Common Aliases
|
||||
|
||||
- `coolify app` | `coolify apps` | `coolify application` | `coolify applications`
|
||||
- `coolify service` | `coolify services` | `coolify svc`
|
||||
- `coolify database` | `coolify databases` | `coolify db` | `coolify dbs`
|
||||
- `coolify teams` | `coolify team`
|
||||
|
||||
## Full Reference
|
||||
|
||||
- Full command and parameter catalog: ./llms-full.txt
|
||||
- Regenerate docs: `go run ./coolify docs llms`
|
||||
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"id": 1,
|
||||
"uuid": "app-fixture-uuid-123",
|
||||
"name": "My Web Application",
|
||||
"description": "A comprehensive test fixture",
|
||||
"status": "running",
|
||||
"fqdn": "https://app.example.com",
|
||||
"git_repository": "https://github.com/example/app",
|
||||
"git_branch": "main",
|
||||
"git_commit_sha": "abc123def456",
|
||||
"git_full_url": "https://github.com/example/app.git",
|
||||
"build_pack": "nixpacks",
|
||||
"ports_exposes": "3000",
|
||||
"ports_mappings": "3000:3000",
|
||||
"domains": "app.example.com",
|
||||
"redirect": "www",
|
||||
"install_command": "npm install",
|
||||
"build_command": "npm run build",
|
||||
"start_command": "npm start",
|
||||
"base_directory": "/",
|
||||
"publish_directory": "/dist",
|
||||
"dockerfile": null,
|
||||
"dockerfile_location": "/Dockerfile",
|
||||
"docker_registry_image_name": null,
|
||||
"docker_registry_image_tag": null,
|
||||
"custom_docker_run_options": null,
|
||||
"custom_labels": null,
|
||||
"custom_nginx_configuration": null,
|
||||
"health_check_enabled": true,
|
||||
"health_check_path": "/health",
|
||||
"health_check_port": "3000",
|
||||
"health_check_host": null,
|
||||
"health_check_method": "GET",
|
||||
"health_check_scheme": "http",
|
||||
"health_check_return_code": 200,
|
||||
"health_check_response_text": null,
|
||||
"health_check_interval": 30,
|
||||
"health_check_timeout": 5,
|
||||
"health_check_retries": 3,
|
||||
"health_check_start_period": 10,
|
||||
"limits_cpus": "2",
|
||||
"limits_memory": "512M",
|
||||
"limits_memory_swap": "1G",
|
||||
"limits_memory_swappiness": 60,
|
||||
"limits_memory_reservation": "256M",
|
||||
"limits_cpuset": null,
|
||||
"limits_cpu_shares": 1024,
|
||||
"pre_deployment_command": "npm run migrate",
|
||||
"pre_deployment_command_container": null,
|
||||
"post_deployment_command": null,
|
||||
"post_deployment_command_container": null,
|
||||
"watch_paths": null,
|
||||
"swarm_replicas": 1,
|
||||
"config_hash": "abc123",
|
||||
"environment_id": 1,
|
||||
"destination_id": 2,
|
||||
"source_id": null,
|
||||
"private_key_id": null,
|
||||
"settings": {
|
||||
"id": 1,
|
||||
"application_id": 1,
|
||||
"is_static": false,
|
||||
"is_build_server_enabled": false,
|
||||
"is_preserve_repository_enabled": false,
|
||||
"is_auto_deploy_enabled": true,
|
||||
"is_force_https_enabled": true,
|
||||
"is_debug_enabled": false,
|
||||
"is_preview_deployments_enabled": true,
|
||||
"is_git_submodules_enabled": false,
|
||||
"is_git_lfs_enabled": false,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-02T00:00:00Z"
|
||||
},
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-02T00:00:00Z"
|
||||
}
|
||||
+2
-1
@@ -8,5 +8,6 @@
|
||||
"is_literal": false,
|
||||
"is_shown_once": false,
|
||||
"is_runtime": true,
|
||||
"is_shared": false
|
||||
"is_shared": false,
|
||||
"comment": "Primary database connection string"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user