mirror of
https://github.com/coollabsio/coolify-cli.git
synced 2026-06-20 16:15:04 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd65345df8 | |||
| c94e147639 | |||
| 0ea34284ef | |||
| a93872ee16 | |||
| 1bc1a601a8 | |||
| 4ad94e2d65 | |||
| faa8186301 | |||
| 1eba511544 | |||
| 541f633edc | |||
| 0f23b029f0 | |||
| 780b3674c7 | |||
| 77adbfaebc | |||
| 9215fd537e | |||
| 26c0925854 | |||
| 1f1b187ed2 | |||
| 4af598c213 | |||
| 6ca3b700ce | |||
| 8cf0b71ebf | |||
| 99a40bfa1d | |||
| 188834fd6d | |||
| f9c3b9869a | |||
| 6044a2107e | |||
| 2f9dc6e8d7 | |||
| 1e741309cb | |||
| 51f759c38f | |||
| 51e9ec5ec8 | |||
| e071fd81d4 | |||
| cb0bbfc5cb | |||
| 87b6b8fdf7 | |||
| 3dbe2507f4 | |||
| 1e82217a50 | |||
| 6f33fa00f1 | |||
| d7841b3b5a | |||
| 234f6e9ed6 | |||
| fa86ceb5cc | |||
| 646bf9de36 | |||
| be29a6e05d |
@@ -49,3 +49,8 @@ jobs:
|
||||
git add internal/version/checker.go
|
||||
git commit -m "chore: bump version to $TAG"
|
||||
git push origin v4.x
|
||||
|
||||
# Move the tag to point to the new commit with updated version
|
||||
git tag -d "$TAG" || true
|
||||
git tag "$TAG"
|
||||
git push origin "refs/tags/$TAG" --force
|
||||
|
||||
+15
-1
@@ -22,4 +22,18 @@ builds:
|
||||
- amd64
|
||||
- arm64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- CGO_ENABLED=0
|
||||
|
||||
checksum:
|
||||
name_template: checksums.txt
|
||||
algorithm: sha256
|
||||
|
||||
archives:
|
||||
- id: coolify-archive
|
||||
ids:
|
||||
- coolify
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
formats: [zip]
|
||||
@@ -9,6 +9,7 @@ This is a CLI tool for interacting with the Coolify API, built with Go using the
|
||||
### API Specification
|
||||
This CLI is a client for the Coolify API. The API specification is defined in the OpenAPI schema:
|
||||
- **Source**: https://github.com/coollabsio/coolify/blob/v4.x/openapi.json
|
||||
- **Raw JSON**: https://raw.githubusercontent.com/coollabsio/coolify/refs/heads/v4.x/openapi.json
|
||||
- **Base Path**: `/api/v1/`
|
||||
- **Authentication**: Bearer token (API tokens from Coolify dashboard at `/security/api-tokens`)
|
||||
|
||||
|
||||
@@ -4,12 +4,32 @@
|
||||
|
||||
### Install script (recommended)
|
||||
|
||||
#### Linux/macOS
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.sh | bash
|
||||
```
|
||||
|
||||
It will install the CLI in `/usr/local/bin/coolify` and the configuration file in `~/.config/coolify/config.json`
|
||||
|
||||
#### Windows (PowerShell)
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
It will install the CLI in `%ProgramFiles%\Coolify\coolify.exe` and the configuration file in `%USERPROFILE%\.config\coolify\config.json`
|
||||
|
||||
For user installation (no admin rights required):
|
||||
```powershell
|
||||
$env:COOLIFY_USER_INSTALL=1; irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
For a specific version:
|
||||
```powershell
|
||||
$env:COOLIFY_VERSION='v1.0.0'; irm https://raw.githubusercontent.com/coollabsio/coolify-cli/main/scripts/install.ps1 | iex
|
||||
```
|
||||
|
||||
### Using `go install`
|
||||
|
||||
```bash
|
||||
@@ -129,7 +149,14 @@ 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> <env_uuid>` - Update an environment variable
|
||||
- `coolify app env update <app_uuid>` - Update an environment variable
|
||||
- `--key <key>` - Variable key (required)
|
||||
- `--value <value>` - Variable value (required)
|
||||
- `--preview` - Available in preview deployments
|
||||
- `--build-time` - Available at build time
|
||||
- `--is-literal` - Treat value as literal (don't interpolate variables)
|
||||
- `--is-multiline` - Value is multiline
|
||||
- `--runtime` - Available at runtime
|
||||
- `coolify app env delete <app_uuid> <env_uuid>` - Delete an environment variable
|
||||
- `coolify app env sync <app_uuid>` - Sync environment variables from a .env file
|
||||
- `--file <path>` - Path to .env file (required)
|
||||
@@ -212,7 +239,13 @@ 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> <env_uuid>` - Update an environment variable
|
||||
- `coolify service env update <service_uuid>` - Update an environment variable
|
||||
- `--key <key>` - Variable key (required)
|
||||
- `--value <value>` - Variable value (required)
|
||||
- `--build-time` - Available at build time
|
||||
- `--is-literal` - Treat value as literal (don't interpolate variables)
|
||||
- `--is-multiline` - Value is multiline
|
||||
- `--runtime` - Available at runtime
|
||||
- `coolify service env delete <service_uuid> <env_uuid>` - Delete an environment variable
|
||||
- `coolify service env sync <service_uuid>` - Sync environment variables from a .env file
|
||||
- `--file <path>` - Path to .env file (required)
|
||||
|
||||
@@ -3,6 +3,7 @@ package application
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/cmd/application/create"
|
||||
"github.com/coollabsio/coolify-cli/cmd/application/env"
|
||||
)
|
||||
|
||||
@@ -18,6 +19,7 @@ func NewAppCommand() *cobra.Command {
|
||||
// Add main subcommands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
cmd.AddCommand(create.NewCreateCommand())
|
||||
cmd.AddCommand(NewUpdateCommand())
|
||||
cmd.AddCommand(NewDeleteCommand())
|
||||
cmd.AddCommand(NewStartCommand())
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package create
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
// NewCreateCommand creates the create parent command with all subcommands
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new application",
|
||||
Long: `Create a new application from various sources.
|
||||
|
||||
Available source types:
|
||||
public Create from a public git repository
|
||||
github Create from a private repository using GitHub App
|
||||
deploy-key Create from a private repository using SSH deploy key
|
||||
dockerfile Create from a custom Dockerfile
|
||||
dockerimage Create from a pre-built Docker image
|
||||
|
||||
Examples:
|
||||
coolify app create public --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--git-repository "https://github.com/user/repo" --git-branch main --build-pack nixpacks --ports-exposes 3000
|
||||
|
||||
coolify app create github --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--github-app-uuid <uuid> --git-repository "user/repo" --git-branch main --build-pack nixpacks --ports-exposes 3000
|
||||
|
||||
coolify app create dockerimage --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--docker-registry-image-name "nginx:latest" --ports-exposes 80`,
|
||||
}
|
||||
|
||||
// Add all create subcommands
|
||||
cmd.AddCommand(NewPublicCommand())
|
||||
cmd.AddCommand(NewGitHubCommand())
|
||||
cmd.AddCommand(NewDeployKeyCommand())
|
||||
cmd.AddCommand(NewDockerfileCommand())
|
||||
cmd.AddCommand(NewDockerImageCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package create
|
||||
|
||||
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/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewDeployKeyCommand returns the create deploy-key application command
|
||||
func NewDeployKeyCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "deploy-key",
|
||||
Short: "Create an application from a private repository using SSH deploy key",
|
||||
Long: `Create a new application from a private git repository using SSH deploy key authentication.
|
||||
|
||||
Use 'coolify privatekeys list' to find your private key UUID.
|
||||
|
||||
Examples:
|
||||
coolify app create deploy-key --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--private-key-uuid <uuid> --git-repository "git@github.com:owner/repo.git" --git-branch main \
|
||||
--build-pack nixpacks --ports-exposes 3000
|
||||
|
||||
coolify app create deploy-key --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--private-key-uuid <uuid> --git-repository "git@gitlab.com:owner/repo.git" --git-branch main \
|
||||
--build-pack dockerfile --ports-exposes 8080 --instant-deploy`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get required flags
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
privateKeyUUID, _ := cmd.Flags().GetString("private-key-uuid")
|
||||
gitRepository, _ := cmd.Flags().GetString("git-repository")
|
||||
gitBranch, _ := cmd.Flags().GetString("git-branch")
|
||||
buildPack, _ := cmd.Flags().GetString("build-pack")
|
||||
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
// Validate required fields
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
if privateKeyUUID == "" {
|
||||
return fmt.Errorf("--private-key-uuid is required")
|
||||
}
|
||||
if gitRepository == "" || gitBranch == "" {
|
||||
return fmt.Errorf("--git-repository and --git-branch are required")
|
||||
}
|
||||
if buildPack == "" || portsExposes == "" {
|
||||
return fmt.Errorf("--build-pack and --ports-exposes are required")
|
||||
}
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ApplicationCreateDeployKeyRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
PrivateKeyUUID: privateKeyUUID,
|
||||
GitRepository: gitRepository,
|
||||
GitBranch: gitBranch,
|
||||
BuildPack: buildPack,
|
||||
PortsExposes: portsExposes,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
setOptionalStringFlag(cmd, "name", &req.Name)
|
||||
setOptionalStringFlag(cmd, "description", &req.Description)
|
||||
setOptionalStringFlag(cmd, "domains", &req.Domains)
|
||||
setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA)
|
||||
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
|
||||
setOptionalStringFlag(cmd, "build-command", &req.BuildCommand)
|
||||
setOptionalStringFlag(cmd, "start-command", &req.StartCommand)
|
||||
setOptionalStringFlag(cmd, "install-command", &req.InstallCommand)
|
||||
setOptionalStringFlag(cmd, "base-directory", &req.BaseDirectory)
|
||||
setOptionalStringFlag(cmd, "publish-directory", &req.PublishDirectory)
|
||||
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
|
||||
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
|
||||
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
|
||||
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
|
||||
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
|
||||
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
app, err := appSvc.CreateDeployKey(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create application: %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(app)
|
||||
},
|
||||
}
|
||||
|
||||
// Required flags
|
||||
cmd.Flags().String("server-uuid", "", "Server UUID (required)")
|
||||
cmd.Flags().String("project-uuid", "", "Project UUID (required)")
|
||||
cmd.Flags().String("environment-name", "", "Environment name")
|
||||
cmd.Flags().String("environment-uuid", "", "Environment UUID")
|
||||
cmd.Flags().String("private-key-uuid", "", "Private key UUID (required)")
|
||||
cmd.Flags().String("git-repository", "", "Git repository SSH URL, e.g., 'git@github.com:owner/repo.git' (required)")
|
||||
cmd.Flags().String("git-branch", "", "Git branch (required)")
|
||||
cmd.Flags().String("build-pack", "", "Build pack: nixpacks, static, dockerfile, dockercompose (required)")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080' (required)")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("domains", "", "Domain(s) for the application")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("git-commit-sha", "", "Specific commit SHA to deploy")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("build-command", "", "Custom build command")
|
||||
cmd.Flags().String("start-command", "", "Custom start command")
|
||||
cmd.Flags().String("install-command", "", "Custom install command")
|
||||
cmd.Flags().String("base-directory", "", "Base directory for the application")
|
||||
cmd.Flags().String("publish-directory", "", "Publish directory for static builds")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
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")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package create
|
||||
|
||||
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/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewDockerfileCommand returns the create dockerfile application command
|
||||
func NewDockerfileCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dockerfile",
|
||||
Short: "Create an application from a custom Dockerfile",
|
||||
Long: `Create a new application from a custom Dockerfile content.
|
||||
|
||||
Examples:
|
||||
coolify app create dockerfile --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--dockerfile "FROM node:18\nWORKDIR /app\nCOPY . .\nRUN npm install\nCMD [\"npm\", \"start\"]"
|
||||
|
||||
coolify app create dockerfile --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--dockerfile "$(cat Dockerfile)" --ports-exposes 3000 --instant-deploy`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get required flags
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
dockerfile, _ := cmd.Flags().GetString("dockerfile")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
// Validate required fields
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
if dockerfile == "" {
|
||||
return fmt.Errorf("--dockerfile is required")
|
||||
}
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ApplicationCreateDockerfileRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
Dockerfile: dockerfile,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
setOptionalStringFlag(cmd, "name", &req.Name)
|
||||
setOptionalStringFlag(cmd, "description", &req.Description)
|
||||
setOptionalStringFlag(cmd, "domains", &req.Domains)
|
||||
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
|
||||
setOptionalStringFlag(cmd, "ports-exposes", &req.PortsExposes)
|
||||
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
|
||||
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
|
||||
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
|
||||
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
|
||||
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
|
||||
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
app, err := appSvc.CreateDockerfile(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create application: %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(app)
|
||||
},
|
||||
}
|
||||
|
||||
// Required flags
|
||||
cmd.Flags().String("server-uuid", "", "Server UUID (required)")
|
||||
cmd.Flags().String("project-uuid", "", "Project UUID (required)")
|
||||
cmd.Flags().String("environment-name", "", "Environment name")
|
||||
cmd.Flags().String("environment-uuid", "", "Environment UUID")
|
||||
cmd.Flags().String("dockerfile", "", "Dockerfile content (required)")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("domains", "", "Domain(s) for the application")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080'")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
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")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package create
|
||||
|
||||
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/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewDockerImageCommand returns the create dockerimage application command
|
||||
func NewDockerImageCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dockerimage",
|
||||
Short: "Create an application from a pre-built Docker image",
|
||||
Long: `Create a new application from a pre-built Docker image from a registry.
|
||||
|
||||
Examples:
|
||||
coolify app create dockerimage --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--docker-registry-image-name "nginx:latest" --ports-exposes 80
|
||||
|
||||
coolify app create dockerimage --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--docker-registry-image-name "ghcr.io/myorg/myapp" --docker-registry-image-tag "v1.0.0" \
|
||||
--ports-exposes 3000 --domains "myapp.example.com" --instant-deploy`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get required flags
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
dockerRegistryImageName, _ := cmd.Flags().GetString("docker-registry-image-name")
|
||||
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
// Validate required fields
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
if dockerRegistryImageName == "" {
|
||||
return fmt.Errorf("--docker-registry-image-name is required")
|
||||
}
|
||||
if portsExposes == "" {
|
||||
return fmt.Errorf("--ports-exposes is required")
|
||||
}
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ApplicationCreateDockerImageRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
DockerRegistryImageName: dockerRegistryImageName,
|
||||
PortsExposes: portsExposes,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
setOptionalStringFlag(cmd, "name", &req.Name)
|
||||
setOptionalStringFlag(cmd, "description", &req.Description)
|
||||
setOptionalStringFlag(cmd, "domains", &req.Domains)
|
||||
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
|
||||
setOptionalStringFlag(cmd, "docker-registry-image-tag", &req.DockerRegistryImageTag)
|
||||
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
|
||||
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
|
||||
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
|
||||
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
|
||||
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
|
||||
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
app, err := appSvc.CreateDockerImage(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create application: %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(app)
|
||||
},
|
||||
}
|
||||
|
||||
// Required flags
|
||||
cmd.Flags().String("server-uuid", "", "Server UUID (required)")
|
||||
cmd.Flags().String("project-uuid", "", "Project UUID (required)")
|
||||
cmd.Flags().String("environment-name", "", "Environment name")
|
||||
cmd.Flags().String("environment-uuid", "", "Environment UUID")
|
||||
cmd.Flags().String("docker-registry-image-name", "", "Docker image name from registry (required)")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '80' or '80,443' (required)")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("domains", "", "Domain(s) for the application")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("docker-registry-image-tag", "", "Docker image tag (defaults to 'latest')")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
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")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package create
|
||||
|
||||
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/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewGitHubCommand returns the create github application command
|
||||
func NewGitHubCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "github",
|
||||
Short: "Create an application from a private repository using GitHub App",
|
||||
Long: `Create a new application from a private git repository using GitHub App authentication.
|
||||
|
||||
Use 'coolify github list' to find your GitHub App UUID.
|
||||
Use 'coolify github repos <app-uuid>' to list accessible repositories.
|
||||
|
||||
Examples:
|
||||
coolify app create github --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--github-app-uuid <uuid> --git-repository "owner/repo" --git-branch main \
|
||||
--build-pack nixpacks --ports-exposes 3000
|
||||
|
||||
coolify app create github --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--github-app-uuid <uuid> --git-repository "owner/repo" --git-branch main \
|
||||
--build-pack dockerfile --ports-exposes 8080 --instant-deploy`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get required flags
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
gitHubAppUUID, _ := cmd.Flags().GetString("github-app-uuid")
|
||||
gitRepository, _ := cmd.Flags().GetString("git-repository")
|
||||
gitBranch, _ := cmd.Flags().GetString("git-branch")
|
||||
buildPack, _ := cmd.Flags().GetString("build-pack")
|
||||
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
// Validate required fields
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
if gitHubAppUUID == "" {
|
||||
return fmt.Errorf("--github-app-uuid is required")
|
||||
}
|
||||
if gitRepository == "" || gitBranch == "" {
|
||||
return fmt.Errorf("--git-repository and --git-branch are required")
|
||||
}
|
||||
if buildPack == "" || portsExposes == "" {
|
||||
return fmt.Errorf("--build-pack and --ports-exposes are required")
|
||||
}
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ApplicationCreateGitHubAppRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
GitHubAppUUID: gitHubAppUUID,
|
||||
GitRepository: gitRepository,
|
||||
GitBranch: gitBranch,
|
||||
BuildPack: buildPack,
|
||||
PortsExposes: portsExposes,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
setOptionalStringFlag(cmd, "name", &req.Name)
|
||||
setOptionalStringFlag(cmd, "description", &req.Description)
|
||||
setOptionalStringFlag(cmd, "domains", &req.Domains)
|
||||
setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA)
|
||||
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
|
||||
setOptionalStringFlag(cmd, "build-command", &req.BuildCommand)
|
||||
setOptionalStringFlag(cmd, "start-command", &req.StartCommand)
|
||||
setOptionalStringFlag(cmd, "install-command", &req.InstallCommand)
|
||||
setOptionalStringFlag(cmd, "base-directory", &req.BaseDirectory)
|
||||
setOptionalStringFlag(cmd, "publish-directory", &req.PublishDirectory)
|
||||
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
|
||||
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
|
||||
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
|
||||
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
|
||||
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
|
||||
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
app, err := appSvc.CreateGitHubApp(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create application: %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(app)
|
||||
},
|
||||
}
|
||||
|
||||
// Required flags
|
||||
cmd.Flags().String("server-uuid", "", "Server UUID (required)")
|
||||
cmd.Flags().String("project-uuid", "", "Project UUID (required)")
|
||||
cmd.Flags().String("environment-name", "", "Environment name")
|
||||
cmd.Flags().String("environment-uuid", "", "Environment UUID")
|
||||
cmd.Flags().String("github-app-uuid", "", "GitHub App UUID (required)")
|
||||
cmd.Flags().String("git-repository", "", "Git repository in format 'owner/repo' (required)")
|
||||
cmd.Flags().String("git-branch", "", "Git branch (required)")
|
||||
cmd.Flags().String("build-pack", "", "Build pack: nixpacks, static, dockerfile, dockercompose (required)")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080' (required)")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("domains", "", "Domain(s) for the application")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("git-commit-sha", "", "Specific commit SHA to deploy")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("build-command", "", "Custom build command")
|
||||
cmd.Flags().String("start-command", "", "Custom start command")
|
||||
cmd.Flags().String("install-command", "", "Custom install command")
|
||||
cmd.Flags().String("base-directory", "", "Base directory for the application")
|
||||
cmd.Flags().String("publish-directory", "", "Publish directory for static builds")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
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")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package create
|
||||
|
||||
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/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewPublicCommand returns the create public application command
|
||||
func NewPublicCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "public",
|
||||
Short: "Create an application from a public git repository",
|
||||
Long: `Create a new application from a public git repository.
|
||||
|
||||
Examples:
|
||||
coolify app create public --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--git-repository "https://github.com/user/repo" --git-branch main --build-pack nixpacks --ports-exposes 3000
|
||||
|
||||
coolify app create public --server-uuid <uuid> --project-uuid <uuid> --environment-name production \
|
||||
--git-repository "https://github.com/user/repo" --git-branch main --build-pack dockerfile --ports-exposes 8080 \
|
||||
--instant-deploy --domains "myapp.example.com"`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get required flags
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
gitRepository, _ := cmd.Flags().GetString("git-repository")
|
||||
gitBranch, _ := cmd.Flags().GetString("git-branch")
|
||||
buildPack, _ := cmd.Flags().GetString("build-pack")
|
||||
portsExposes, _ := cmd.Flags().GetString("ports-exposes")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
// Validate required fields
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
if gitRepository == "" || gitBranch == "" {
|
||||
return fmt.Errorf("--git-repository and --git-branch are required")
|
||||
}
|
||||
if buildPack == "" || portsExposes == "" {
|
||||
return fmt.Errorf("--build-pack and --ports-exposes are required")
|
||||
}
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ApplicationCreatePublicRequest{
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
GitRepository: gitRepository,
|
||||
GitBranch: gitBranch,
|
||||
BuildPack: buildPack,
|
||||
PortsExposes: portsExposes,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = &environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Optional fields
|
||||
setOptionalStringFlag(cmd, "name", &req.Name)
|
||||
setOptionalStringFlag(cmd, "description", &req.Description)
|
||||
setOptionalStringFlag(cmd, "domains", &req.Domains)
|
||||
setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA)
|
||||
setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID)
|
||||
setOptionalStringFlag(cmd, "build-command", &req.BuildCommand)
|
||||
setOptionalStringFlag(cmd, "start-command", &req.StartCommand)
|
||||
setOptionalStringFlag(cmd, "install-command", &req.InstallCommand)
|
||||
setOptionalStringFlag(cmd, "base-directory", &req.BaseDirectory)
|
||||
setOptionalStringFlag(cmd, "publish-directory", &req.PublishDirectory)
|
||||
setOptionalStringFlag(cmd, "ports-mappings", &req.PortsMappings)
|
||||
setOptionalStringFlag(cmd, "limits-cpus", &req.LimitsCPUs)
|
||||
setOptionalStringFlag(cmd, "limits-memory", &req.LimitsMemory)
|
||||
setOptionalBoolFlag(cmd, "instant-deploy", &req.InstantDeploy)
|
||||
setOptionalBoolFlag(cmd, "health-check-enabled", &req.HealthCheckEnabled)
|
||||
setOptionalStringFlag(cmd, "health-check-path", &req.HealthCheckPath)
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
app, err := appSvc.CreatePublic(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create application: %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(app)
|
||||
},
|
||||
}
|
||||
|
||||
// Required flags
|
||||
cmd.Flags().String("server-uuid", "", "Server UUID (required)")
|
||||
cmd.Flags().String("project-uuid", "", "Project UUID (required)")
|
||||
cmd.Flags().String("environment-name", "", "Environment name")
|
||||
cmd.Flags().String("environment-uuid", "", "Environment UUID")
|
||||
cmd.Flags().String("git-repository", "", "Git repository URL (required)")
|
||||
cmd.Flags().String("git-branch", "", "Git branch (required)")
|
||||
cmd.Flags().String("build-pack", "", "Build pack: nixpacks, static, dockerfile, dockercompose (required)")
|
||||
cmd.Flags().String("ports-exposes", "", "Exposed ports, e.g., '3000' or '3000,8080' (required)")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Application name")
|
||||
cmd.Flags().String("description", "", "Application description")
|
||||
cmd.Flags().String("domains", "", "Domain(s) for the application")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("git-commit-sha", "", "Specific commit SHA to deploy")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().String("build-command", "", "Custom build command")
|
||||
cmd.Flags().String("start-command", "", "Custom start command")
|
||||
cmd.Flags().String("install-command", "", "Custom install command")
|
||||
cmd.Flags().String("base-directory", "", "Base directory for the application")
|
||||
cmd.Flags().String("publish-directory", "", "Publish directory for static builds")
|
||||
cmd.Flags().String("ports-mappings", "", "Port mappings (host:container)")
|
||||
cmd.Flags().String("limits-cpus", "", "CPU limit")
|
||||
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")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Helper functions for optional flags
|
||||
func setOptionalStringFlag(cmd *cobra.Command, flagName string, target **string) {
|
||||
if cmd.Flags().Changed(flagName) {
|
||||
val, _ := cmd.Flags().GetString(flagName)
|
||||
*target = &val
|
||||
}
|
||||
}
|
||||
|
||||
func setOptionalBoolFlag(cmd *cobra.Command, flagName string, target **bool) {
|
||||
if cmd.Flags().Changed(flagName) {
|
||||
val, _ := cmd.Flags().GetBool(flagName)
|
||||
*target = &val
|
||||
}
|
||||
}
|
||||
Vendored
+6
-1
@@ -31,6 +31,7 @@ func NewCreateEnvCommand() *cobra.Command {
|
||||
isPreview, _ := cmd.Flags().GetBool("preview")
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
|
||||
if key == "" {
|
||||
return fmt.Errorf("--key is required")
|
||||
@@ -56,6 +57,9 @@ func NewCreateEnvCommand() *cobra.Command {
|
||||
if cmd.Flags().Changed("is-multiline") {
|
||||
req.IsMultiline = &isMultiline
|
||||
}
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
env, err := appSvc.CreateEnv(ctx, appUUID, req)
|
||||
@@ -71,9 +75,10 @@ func NewCreateEnvCommand() *cobra.Command {
|
||||
|
||||
cmd.Flags().String("key", "", "Environment variable key (required)")
|
||||
cmd.Flags().String("value", "", "Environment variable value (required)")
|
||||
cmd.Flags().Bool("build-time", false, "Available at build time")
|
||||
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 (don't interpolate variables)")
|
||||
cmd.Flags().Bool("is-multiline", false, "Value is multiline")
|
||||
cmd.Flags().Bool("runtime", true, "Available at runtime (default: true)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
Vendored
+5
-1
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func NewGetEnvCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
cmd := &cobra.Command{
|
||||
Use: "get <app_uuid> <env_uuid_or_key>",
|
||||
Short: "Get environment variable details",
|
||||
Long: `Get detailed information about a specific environment variable by UUID or key name.`,
|
||||
@@ -27,6 +27,8 @@ func NewGetEnvCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
|
||||
// First try to get by the identifier directly
|
||||
env, err := appSvc.GetEnv(ctx, appUUID, envUUIDOrKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get environment variable: %w", err)
|
||||
@@ -53,4 +55,6 @@ func NewGetEnvCommand() *cobra.Command {
|
||||
return formatter.Format(env)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
Vendored
+32
-2
@@ -2,19 +2,21 @@ package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/internal/cli"
|
||||
"github.com/coollabsio/coolify-cli/internal/models"
|
||||
"github.com/coollabsio/coolify-cli/internal/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
func NewListEnvCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
cmd := &cobra.Command{
|
||||
Use: "list <app_uuid>",
|
||||
Short: "List all environment variables for an application",
|
||||
Long: `List all environment variables for a specific application.`,
|
||||
Long: `List all environment variables for a specific application. By default, only non-preview environment variables are shown. Use --preview to show preview environment variables instead, or --all to show all variables (non-preview first, then preview).`,
|
||||
Args: cli.ExactArgs(1, "<uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
@@ -31,6 +33,29 @@ func NewListEnvCommand() *cobra.Command {
|
||||
return fmt.Errorf("failed to list environment variables: %w", err)
|
||||
}
|
||||
|
||||
// Filter by preview/all flags
|
||||
showAll, _ := cmd.Flags().GetBool("all")
|
||||
showPreview, _ := cmd.Flags().GetBool("preview")
|
||||
|
||||
if showAll {
|
||||
// Sort: non-preview first, then preview
|
||||
sort.SliceStable(envs, func(i, j int) bool {
|
||||
if envs[i].IsPreview != envs[j].IsPreview {
|
||||
return !envs[i].IsPreview // non-preview (false) comes before preview (true)
|
||||
}
|
||||
return false // maintain original order within groups
|
||||
})
|
||||
} else {
|
||||
// Filter by preview flag
|
||||
var filtered []models.EnvironmentVariable
|
||||
for _, env := range envs {
|
||||
if env.IsPreview == showPreview {
|
||||
filtered = append(filtered, env)
|
||||
}
|
||||
}
|
||||
envs = filtered
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
showSensitive, _ := cmd.Flags().GetBool("show-sensitive")
|
||||
|
||||
@@ -54,4 +79,9 @@ func NewListEnvCommand() *cobra.Command {
|
||||
return formatter.Format(envs)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("preview", false, "Show preview environment variables instead of regular ones")
|
||||
cmd.Flags().Bool("all", false, "Show all environment variables (non-preview first, then preview)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
Vendored
+6
-1
@@ -40,6 +40,7 @@ Example: coolify app env sync abc123 --file .env.production`,
|
||||
isBuildTime, _ := cmd.Flags().GetBool("build-time")
|
||||
isPreview, _ := cmd.Flags().GetBool("preview")
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
|
||||
// Parse the .env file
|
||||
envVars, err := parser.ParseEnvFile(filePath)
|
||||
@@ -87,6 +88,9 @@ Example: coolify app env sync abc123 --file .env.production`,
|
||||
if cmd.Flags().Changed("is-literal") {
|
||||
req.IsLiteral = &isLiteral
|
||||
}
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
// Auto-detect multiline values
|
||||
if strings.Contains(envVar.Value, "\n") {
|
||||
@@ -147,8 +151,9 @@ Example: coolify app env sync abc123 --file .env.production`,
|
||||
}
|
||||
|
||||
syncEnvCmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
|
||||
syncEnvCmd.Flags().Bool("build-time", false, "Make all variables available at build time")
|
||||
syncEnvCmd.Flags().Bool("build-time", true, "Make all variables available at build time (default: true)")
|
||||
syncEnvCmd.Flags().Bool("preview", false, "Make all variables available in preview deployments")
|
||||
syncEnvCmd.Flags().Bool("is-literal", false, "Treat all values as literal (don't interpolate variables)")
|
||||
syncEnvCmd.Flags().Bool("runtime", true, "Make all variables available at runtime (default: true)")
|
||||
return syncEnvCmd
|
||||
}
|
||||
|
||||
Vendored
+19
-9
@@ -12,24 +12,26 @@ import (
|
||||
|
||||
func NewUpdateEnvCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update <app_uuid> <env_uuid>",
|
||||
Use: "update <app_uuid>",
|
||||
Short: "Update an environment variable",
|
||||
Long: `Update an existing environment variable. First UUID is the application, second is the specific environment variable to update.`,
|
||||
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
|
||||
Long: `Update an existing environment variable. UUID is the application.`,
|
||||
Args: cli.ExactArgs(1, "<app_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
appUUID := args[0]
|
||||
envUUID := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
req := &models.EnvironmentVariableUpdateRequest{
|
||||
UUID: envUUID,
|
||||
// Check minimum version requirement
|
||||
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.469"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &models.EnvironmentVariableUpdateRequest{}
|
||||
|
||||
if cmd.Flags().Changed("key") {
|
||||
key, _ := cmd.Flags().GetString("key")
|
||||
req.Key = &key
|
||||
@@ -54,9 +56,16 @@ func NewUpdateEnvCommand() *cobra.Command {
|
||||
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
|
||||
req.IsMultiline = &isMultiline
|
||||
}
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil {
|
||||
return fmt.Errorf("at least one field must be provided to update")
|
||||
if req.Key == nil {
|
||||
return fmt.Errorf("--key is required")
|
||||
}
|
||||
if req.Value == nil {
|
||||
return fmt.Errorf("--value is required")
|
||||
}
|
||||
|
||||
appSvc := service.NewApplicationService(client)
|
||||
@@ -72,9 +81,10 @@ func NewUpdateEnvCommand() *cobra.Command {
|
||||
|
||||
cmd.Flags().String("key", "", "New environment variable key")
|
||||
cmd.Flags().String("value", "", "New environment variable value")
|
||||
cmd.Flags().Bool("build-time", false, "Available at build time")
|
||||
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)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package project
|
||||
|
||||
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/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// NewCreateCommand returns the create project command
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a new project",
|
||||
Long: `Create a new project in Coolify.
|
||||
|
||||
Examples:
|
||||
coolify project create --name "My Project"
|
||||
coolify project create --name "My Project" --description "A description"`,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
if name == "" {
|
||||
return fmt.Errorf("--name is required")
|
||||
}
|
||||
|
||||
req := &models.ProjectCreateRequest{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("description") {
|
||||
desc, _ := cmd.Flags().GetString("description")
|
||||
req.Description = &desc
|
||||
}
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
projectSvc := service.NewProjectService(client)
|
||||
project, err := projectSvc.Create(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create project: %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(project)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().String("name", "", "Project name (required)")
|
||||
cmd.Flags().String("description", "", "Project description")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -14,6 +14,7 @@ func NewProjectCommand() *cobra.Command {
|
||||
// Add all project subcommands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
cmd.AddCommand(NewCreateCommand())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+2
-21
@@ -6,7 +6,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
compareVersion "github.com/hashicorp/go-version"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
@@ -144,24 +143,6 @@ func initConfig() {
|
||||
// They are loaded on-demand by getAPIClient() based on --instance or default instance
|
||||
// This allows --instance flag to work correctly
|
||||
|
||||
// Check for updates
|
||||
latestVersionStr, err := version.CheckLatestVersionOfCli(Debug)
|
||||
if err != nil {
|
||||
if Debug {
|
||||
log.Println("Failed to check for updates:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Compare versions properly using semantic versioning
|
||||
if latestVersionStr != "" {
|
||||
latestVersion, err := compareVersion.NewVersion(latestVersionStr)
|
||||
if err == nil {
|
||||
currentVersion, err := compareVersion.NewVersion(version.GetVersion())
|
||||
if err == nil && latestVersion.GreaterThan(currentVersion) {
|
||||
if Debug {
|
||||
log.Printf("New version of Coolify CLI is available: %s\n", latestVersionStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for updates (errors are handled silently inside the function)
|
||||
_, _ = version.CheckLatestVersionOfCli(Debug)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package service
|
||||
|
||||
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/output"
|
||||
"github.com/coollabsio/coolify-cli/internal/service"
|
||||
)
|
||||
|
||||
// validServiceTypes contains all supported one-click service types
|
||||
var validServiceTypes = []string{
|
||||
"activepieces",
|
||||
"appsmith",
|
||||
"appwrite",
|
||||
"authentik",
|
||||
"babybuddy",
|
||||
"budge",
|
||||
"changedetection",
|
||||
"chatwoot",
|
||||
"classicpress-with-mariadb",
|
||||
"classicpress-with-mysql",
|
||||
"classicpress-without-database",
|
||||
"cloudflared",
|
||||
"code-server",
|
||||
"dashboard",
|
||||
"directus",
|
||||
"directus-with-postgresql",
|
||||
"docker-registry",
|
||||
"docuseal",
|
||||
"docuseal-with-postgres",
|
||||
"dokuwiki",
|
||||
"duplicati",
|
||||
"emby",
|
||||
"embystat",
|
||||
"fider",
|
||||
"filebrowser",
|
||||
"firefly",
|
||||
"formbricks",
|
||||
"ghost",
|
||||
"gitea",
|
||||
"gitea-with-mariadb",
|
||||
"gitea-with-mysql",
|
||||
"gitea-with-postgresql",
|
||||
"glance",
|
||||
"glances",
|
||||
"glitchtip",
|
||||
"grafana",
|
||||
"grafana-with-postgresql",
|
||||
"grocy",
|
||||
"heimdall",
|
||||
"homepage",
|
||||
"jellyfin",
|
||||
"kuzzle",
|
||||
"listmonk",
|
||||
"logto",
|
||||
"mediawiki",
|
||||
"meilisearch",
|
||||
"metabase",
|
||||
"metube",
|
||||
"minio",
|
||||
"moodle",
|
||||
"n8n",
|
||||
"n8n-with-postgresql",
|
||||
"next-image-transformation",
|
||||
"nextcloud",
|
||||
"nocodb",
|
||||
"odoo",
|
||||
"openblocks",
|
||||
"pairdrop",
|
||||
"penpot",
|
||||
"phpmyadmin",
|
||||
"pocketbase",
|
||||
"posthog",
|
||||
"reactive-resume",
|
||||
"rocketchat",
|
||||
"shlink",
|
||||
"slash",
|
||||
"snapdrop",
|
||||
"statusnook",
|
||||
"stirling-pdf",
|
||||
"supabase",
|
||||
"syncthing",
|
||||
"tolgee",
|
||||
"trigger",
|
||||
"trigger-with-external-database",
|
||||
"twenty",
|
||||
"umami",
|
||||
"unleash-with-postgresql",
|
||||
"unleash-without-database",
|
||||
"uptime-kuma",
|
||||
"vaultwarden",
|
||||
"vikunja",
|
||||
"weblate",
|
||||
"whoogle",
|
||||
"wordpress-with-mariadb",
|
||||
"wordpress-with-mysql",
|
||||
"wordpress-without-database",
|
||||
}
|
||||
|
||||
func NewCreateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create <type>",
|
||||
Short: "Create a new one-click service",
|
||||
Long: `Create a new one-click service of the specified type.
|
||||
|
||||
Use 'coolify service create --list-types' to see all available service types.
|
||||
|
||||
Examples:
|
||||
coolify service create wordpress-with-mysql --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production
|
||||
coolify service create ghost --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production --name="My Blog"
|
||||
coolify service create n8n --server-uuid=<uuid> --project-uuid=<uuid> --environment-name=production --instant-deploy
|
||||
|
||||
Popular service types:
|
||||
- wordpress-with-mysql, wordpress-with-mariadb, wordpress-without-database
|
||||
- ghost, plausible, umami, uptime-kuma
|
||||
- n8n, n8n-with-postgresql
|
||||
- nextcloud, gitea, minio
|
||||
- grafana, metabase, nocodb
|
||||
- supabase, pocketbase, appwrite`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Handle --list-types flag
|
||||
listTypes, _ := cmd.Flags().GetBool("list-types")
|
||||
if listTypes {
|
||||
fmt.Println("Available one-click service types:")
|
||||
fmt.Println()
|
||||
for _, t := range validServiceTypes {
|
||||
fmt.Printf(" %s\n", t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Require type argument if not listing
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("service type is required. Use --list-types to see available types")
|
||||
}
|
||||
|
||||
serviceType := args[0]
|
||||
|
||||
// Validate service type
|
||||
isValid := false
|
||||
for _, t := range validServiceTypes {
|
||||
if t == serviceType {
|
||||
isValid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isValid {
|
||||
return fmt.Errorf("invalid service type '%s'. Use --list-types to see available types", serviceType)
|
||||
}
|
||||
|
||||
serverUUID, _ := cmd.Flags().GetString("server-uuid")
|
||||
projectUUID, _ := cmd.Flags().GetString("project-uuid")
|
||||
environmentName, _ := cmd.Flags().GetString("environment-name")
|
||||
environmentUUID, _ := cmd.Flags().GetString("environment-uuid")
|
||||
|
||||
if serverUUID == "" || projectUUID == "" {
|
||||
return fmt.Errorf("--server-uuid and --project-uuid are required")
|
||||
}
|
||||
|
||||
if environmentName == "" && environmentUUID == "" {
|
||||
return fmt.Errorf("either --environment-name or --environment-uuid must be provided")
|
||||
}
|
||||
|
||||
req := &models.ServiceCreateRequest{
|
||||
Type: serviceType,
|
||||
ServerUUID: serverUUID,
|
||||
ProjectUUID: projectUUID,
|
||||
}
|
||||
|
||||
if environmentName != "" {
|
||||
req.EnvironmentName = environmentName
|
||||
}
|
||||
if environmentUUID != "" {
|
||||
req.EnvironmentUUID = &environmentUUID
|
||||
}
|
||||
|
||||
// Handle optional flags
|
||||
if cmd.Flags().Changed("name") {
|
||||
name, _ := cmd.Flags().GetString("name")
|
||||
req.Name = &name
|
||||
}
|
||||
if cmd.Flags().Changed("description") {
|
||||
desc, _ := cmd.Flags().GetString("description")
|
||||
req.Description = &desc
|
||||
}
|
||||
if cmd.Flags().Changed("destination-uuid") {
|
||||
dest, _ := cmd.Flags().GetString("destination-uuid")
|
||||
req.Destination = &dest
|
||||
}
|
||||
if cmd.Flags().Changed("instant-deploy") {
|
||||
instant, _ := cmd.Flags().GetBool("instant-deploy")
|
||||
req.InstantDeploy = &instant
|
||||
}
|
||||
if cmd.Flags().Changed("docker-compose") {
|
||||
compose, _ := cmd.Flags().GetString("docker-compose")
|
||||
req.DockerCompose = &compose
|
||||
}
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
svc := service.NewService(client)
|
||||
result, err := svc.Create(ctx, req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create service: %w", err)
|
||||
}
|
||||
|
||||
format, _ := cmd.Flags().GetString("format")
|
||||
formatter, err := output.NewFormatter(format, output.Options{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create formatter: %w", err)
|
||||
}
|
||||
|
||||
return formatter.Format(result)
|
||||
},
|
||||
}
|
||||
|
||||
// List types flag
|
||||
cmd.Flags().Bool("list-types", false, "List all available service types")
|
||||
|
||||
// Required flags
|
||||
cmd.Flags().String("server-uuid", "", "Server UUID (required)")
|
||||
cmd.Flags().String("project-uuid", "", "Project UUID (required)")
|
||||
cmd.Flags().String("environment-name", "", "Environment name")
|
||||
cmd.Flags().String("environment-uuid", "", "Environment UUID")
|
||||
|
||||
// Optional flags
|
||||
cmd.Flags().String("name", "", "Service name")
|
||||
cmd.Flags().String("description", "", "Service description")
|
||||
cmd.Flags().String("destination-uuid", "", "Destination UUID if server has multiple destinations")
|
||||
cmd.Flags().Bool("instant-deploy", false, "Deploy immediately after creation")
|
||||
cmd.Flags().String("docker-compose", "", "Custom Docker Compose content (for advanced customization)")
|
||||
|
||||
// Add completion for service type positional argument
|
||||
cmd.ValidArgsFunction = func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return validServiceTypes, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
Vendored
+7
-7
@@ -28,9 +28,9 @@ func NewCreateCommand() *cobra.Command {
|
||||
key, _ := cmd.Flags().GetString("key")
|
||||
value, _ := cmd.Flags().GetString("value")
|
||||
isBuildTime, _ := cmd.Flags().GetBool("build-time")
|
||||
isPreview, _ := cmd.Flags().GetBool("preview")
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
|
||||
if key == "" {
|
||||
return fmt.Errorf("--key is required")
|
||||
@@ -39,7 +39,7 @@ func NewCreateCommand() *cobra.Command {
|
||||
return fmt.Errorf("--value is required")
|
||||
}
|
||||
|
||||
req := &models.EnvironmentVariableCreateRequest{
|
||||
req := &models.ServiceEnvironmentVariableCreateRequest{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
@@ -48,15 +48,15 @@ func NewCreateCommand() *cobra.Command {
|
||||
if cmd.Flags().Changed("build-time") {
|
||||
req.IsBuildTime = &isBuildTime
|
||||
}
|
||||
if cmd.Flags().Changed("preview") {
|
||||
req.IsPreview = &isPreview
|
||||
}
|
||||
if cmd.Flags().Changed("is-literal") {
|
||||
req.IsLiteral = &isLiteral
|
||||
}
|
||||
if cmd.Flags().Changed("is-multiline") {
|
||||
req.IsMultiline = &isMultiline
|
||||
}
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
serviceSvc := service.NewService(client)
|
||||
env, err := serviceSvc.CreateEnv(ctx, uuid, req)
|
||||
@@ -71,10 +71,10 @@ func NewCreateCommand() *cobra.Command {
|
||||
|
||||
cmd.Flags().String("key", "", "Environment variable key (required)")
|
||||
cmd.Flags().String("value", "", "Environment variable value (required)")
|
||||
cmd.Flags().Bool("build-time", false, "Available at build time")
|
||||
cmd.Flags().Bool("preview", false, "Available in preview deployments")
|
||||
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)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
Vendored
+11
-11
@@ -38,8 +38,8 @@ Example: coolify service env sync abc123 --file .env.production`,
|
||||
}
|
||||
|
||||
isBuildTime, _ := cmd.Flags().GetBool("build-time")
|
||||
isPreview, _ := cmd.Flags().GetBool("preview")
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
|
||||
// Parse the .env file
|
||||
envVars, err := parser.ParseEnvFile(filePath)
|
||||
@@ -62,17 +62,17 @@ Example: coolify service env sync abc123 --file .env.production`,
|
||||
}
|
||||
|
||||
// Build a map of existing env vars by key
|
||||
existingMap := make(map[string]models.EnvironmentVariable)
|
||||
existingMap := make(map[string]models.ServiceEnvironmentVariable)
|
||||
for _, env := range existingEnvs {
|
||||
existingMap[env.Key] = env
|
||||
}
|
||||
|
||||
// Separate into updates and creates
|
||||
var toUpdate []models.EnvironmentVariableCreateRequest
|
||||
var toCreate []models.EnvironmentVariableCreateRequest
|
||||
var toUpdate []models.ServiceEnvironmentVariableCreateRequest
|
||||
var toCreate []models.ServiceEnvironmentVariableCreateRequest
|
||||
|
||||
for _, envVar := range envVars {
|
||||
req := models.EnvironmentVariableCreateRequest{
|
||||
req := models.ServiceEnvironmentVariableCreateRequest{
|
||||
Key: envVar.Key,
|
||||
Value: envVar.Value,
|
||||
}
|
||||
@@ -81,12 +81,12 @@ Example: coolify service env sync abc123 --file .env.production`,
|
||||
if cmd.Flags().Changed("build-time") {
|
||||
req.IsBuildTime = &isBuildTime
|
||||
}
|
||||
if cmd.Flags().Changed("preview") {
|
||||
req.IsPreview = &isPreview
|
||||
}
|
||||
if cmd.Flags().Changed("is-literal") {
|
||||
req.IsLiteral = &isLiteral
|
||||
}
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
// Auto-detect multiline values
|
||||
if strings.Contains(envVar.Value, "\n") {
|
||||
@@ -108,7 +108,7 @@ Example: coolify service env sync abc123 --file .env.production`,
|
||||
// Perform bulk update if there are vars to update
|
||||
if len(toUpdate) > 0 {
|
||||
fmt.Printf("Updating %d existing variables...\n", len(toUpdate))
|
||||
bulkReq := &service.BulkUpdateEnvsRequest{
|
||||
bulkReq := &models.ServiceEnvBulkUpdateRequest{
|
||||
Data: toUpdate,
|
||||
}
|
||||
_, err := serviceSvc.BulkUpdateEnvs(ctx, uuid, bulkReq)
|
||||
@@ -147,9 +147,9 @@ Example: coolify service env sync abc123 --file .env.production`,
|
||||
}
|
||||
|
||||
cmd.Flags().StringP("file", "f", "", "Path to .env file (required)")
|
||||
cmd.Flags().Bool("build-time", false, "Make all variables available at build time")
|
||||
cmd.Flags().Bool("preview", false, "Make all variables available in preview deployments")
|
||||
cmd.Flags().Bool("build-time", true, "Make all variables available at build time (default: true)")
|
||||
cmd.Flags().Bool("is-literal", false, "Treat all values as literal (don't interpolate variables)")
|
||||
cmd.Flags().Bool("runtime", true, "Make all variables available at runtime (default: true)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
Vendored
+19
-15
@@ -12,24 +12,26 @@ import (
|
||||
|
||||
func NewUpdateCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "update <service_uuid> <env_uuid>",
|
||||
Use: "update <service_uuid>",
|
||||
Short: "Update an environment variable",
|
||||
Long: `Update an existing environment variable. First UUID is the service, second is the specific environment variable to update.`,
|
||||
Args: cli.ExactArgs(2, "<uuid1> <uuid2>"),
|
||||
Long: `Update an existing environment variable. UUID is the service.`,
|
||||
Args: cli.ExactArgs(1, "<service_uuid>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
serviceUUID := args[0]
|
||||
envUUID := args[1]
|
||||
|
||||
client, err := cli.GetAPIClient(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API client: %w", err)
|
||||
}
|
||||
|
||||
req := &models.EnvironmentVariableUpdateRequest{
|
||||
UUID: envUUID,
|
||||
// Check minimum version requirement
|
||||
if err := cli.CheckMinimumVersion(ctx, client, "4.0.0-beta.469"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &models.ServiceEnvironmentVariableUpdateRequest{}
|
||||
|
||||
// Only set fields that were provided
|
||||
if cmd.Flags().Changed("key") {
|
||||
key, _ := cmd.Flags().GetString("key")
|
||||
@@ -43,10 +45,6 @@ func NewUpdateCommand() *cobra.Command {
|
||||
isBuildTime, _ := cmd.Flags().GetBool("build-time")
|
||||
req.IsBuildTime = &isBuildTime
|
||||
}
|
||||
if cmd.Flags().Changed("preview") {
|
||||
isPreview, _ := cmd.Flags().GetBool("preview")
|
||||
req.IsPreview = &isPreview
|
||||
}
|
||||
if cmd.Flags().Changed("is-literal") {
|
||||
isLiteral, _ := cmd.Flags().GetBool("is-literal")
|
||||
req.IsLiteral = &isLiteral
|
||||
@@ -55,10 +53,16 @@ func NewUpdateCommand() *cobra.Command {
|
||||
isMultiline, _ := cmd.Flags().GetBool("is-multiline")
|
||||
req.IsMultiline = &isMultiline
|
||||
}
|
||||
if cmd.Flags().Changed("runtime") {
|
||||
isRuntime, _ := cmd.Flags().GetBool("runtime")
|
||||
req.IsRuntime = &isRuntime
|
||||
}
|
||||
|
||||
// Check if at least one field is being updated
|
||||
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil {
|
||||
return fmt.Errorf("at least one field must be provided to update (--key, --value, --build-time, --preview, --is-literal, or --is-multiline)")
|
||||
if req.Key == nil {
|
||||
return fmt.Errorf("--key is required")
|
||||
}
|
||||
if req.Value == nil {
|
||||
return fmt.Errorf("--value is required")
|
||||
}
|
||||
|
||||
serviceSvc := service.NewService(client)
|
||||
@@ -74,10 +78,10 @@ func NewUpdateCommand() *cobra.Command {
|
||||
|
||||
cmd.Flags().String("key", "", "New environment variable key")
|
||||
cmd.Flags().String("value", "", "New environment variable value")
|
||||
cmd.Flags().Bool("build-time", false, "Available at build time")
|
||||
cmd.Flags().Bool("preview", false, "Available in preview deployments")
|
||||
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)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
+18
-10
@@ -1,6 +1,10 @@
|
||||
package service
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/coollabsio/coolify-cli/cmd/service/env"
|
||||
)
|
||||
|
||||
// NewServiceCommand creates the service parent command with all subcommands
|
||||
func NewServiceCommand() *cobra.Command {
|
||||
@@ -14,20 +18,24 @@ func NewServiceCommand() *cobra.Command {
|
||||
// Add main service commands
|
||||
cmd.AddCommand(NewListCommand())
|
||||
cmd.AddCommand(NewGetCommand())
|
||||
cmd.AddCommand(NewCreateCommand())
|
||||
cmd.AddCommand(NewStartCommand())
|
||||
cmd.AddCommand(NewStopCommand())
|
||||
cmd.AddCommand(NewRestartCommand())
|
||||
cmd.AddCommand(NewDeleteCommand())
|
||||
|
||||
// Add env subcommand (placeholder for now)
|
||||
// TODO: Implement env commands
|
||||
// envCmd := &cobra.Command{
|
||||
// Use: "env",
|
||||
// Short: "Manage service environment variables",
|
||||
// }
|
||||
// envCmd.AddCommand(env.NewListCommand())
|
||||
// ... more env commands
|
||||
// cmd.AddCommand(envCmd)
|
||||
// Add env subcommand
|
||||
envCmd := &cobra.Command{
|
||||
Use: "env",
|
||||
Short: "Manage service 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)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ require (
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/xanzy/go-gitlab v0.115.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -86,8 +86,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
@@ -97,15 +97,15 @@ 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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -100,10 +100,12 @@ type EnvironmentVariable struct {
|
||||
UUID string `json:"uuid"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value" sensitive:"true"`
|
||||
IsBuildTime bool `json:"is_build_time"`
|
||||
IsBuildTime bool `json:"is_buildtime"`
|
||||
IsPreview bool `json:"is_preview"`
|
||||
IsLiteralValue bool `json:"is_literal"`
|
||||
IsShownOnce bool `json:"is_shown_once"`
|
||||
IsRuntime bool `json:"is_runtime"`
|
||||
IsShared bool `json:"is_shared"`
|
||||
RealValue *string `json:"real_value,omitempty" sensitive:"true"`
|
||||
ApplicationID *int `json:"-" table:"-"`
|
||||
CreatedAt string `json:"-" table:"-"`
|
||||
@@ -118,15 +120,197 @@ type EnvironmentVariableCreateRequest struct {
|
||||
IsPreview *bool `json:"is_preview,omitempty"`
|
||||
IsLiteral *bool `json:"is_literal,omitempty"`
|
||||
IsMultiline *bool `json:"is_multiline,omitempty"`
|
||||
IsRuntime *bool `json:"is_runtime,omitempty"`
|
||||
}
|
||||
|
||||
// EnvironmentVariableUpdateRequest represents the request to update an environment variable
|
||||
type EnvironmentVariableUpdateRequest struct {
|
||||
UUID string `json:"uuid"`
|
||||
Key *string `json:"key,omitempty"`
|
||||
Value *string `json:"value,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// ApplicationCreatePublicRequest for POST /applications/public
|
||||
// Creates an application from a public git repository
|
||||
type ApplicationCreatePublicRequest struct {
|
||||
// Required fields
|
||||
ProjectUUID string `json:"project_uuid"`
|
||||
ServerUUID string `json:"server_uuid"`
|
||||
GitRepository string `json:"git_repository"`
|
||||
GitBranch string `json:"git_branch"`
|
||||
BuildPack string `json:"build_pack"` // nixpacks, static, dockerfile, dockercompose
|
||||
PortsExposes string `json:"ports_exposes"`
|
||||
|
||||
// Environment (one of these is required)
|
||||
EnvironmentName *string `json:"environment_name,omitempty"`
|
||||
EnvironmentUUID *string `json:"environment,omitempty"`
|
||||
|
||||
// Optional fields
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Domains *string `json:"domains,omitempty"`
|
||||
InstantDeploy *bool `json:"instant_deploy,omitempty"`
|
||||
GitCommitSHA *string `json:"git_commit_sha,omitempty"`
|
||||
DestinationUUID *string `json:"destination_uuid,omitempty"`
|
||||
BuildCommand *string `json:"build_command,omitempty"`
|
||||
StartCommand *string `json:"start_command,omitempty"`
|
||||
InstallCommand *string `json:"install_command,omitempty"`
|
||||
BaseDirectory *string `json:"base_directory,omitempty"`
|
||||
PublishDirectory *string `json:"publish_directory,omitempty"`
|
||||
PortsMappings *string `json:"ports_mappings,omitempty"`
|
||||
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"`
|
||||
CustomLabels *string `json:"custom_labels,omitempty"`
|
||||
|
||||
// Health checks
|
||||
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"`
|
||||
HealthCheckPath *string `json:"health_check_path,omitempty"`
|
||||
HealthCheckPort *string `json:"health_check_port,omitempty"`
|
||||
HealthCheckMethod *string `json:"health_check_method,omitempty"`
|
||||
|
||||
// Resource limits
|
||||
LimitsCPUs *string `json:"limits_cpus,omitempty"`
|
||||
LimitsMemory *string `json:"limits_memory,omitempty"`
|
||||
}
|
||||
|
||||
// ApplicationCreateGitHubAppRequest for POST /applications/private-github-app
|
||||
// Creates an application from a private repository using GitHub App authentication
|
||||
type ApplicationCreateGitHubAppRequest struct {
|
||||
// Required fields
|
||||
ProjectUUID string `json:"project_uuid"`
|
||||
ServerUUID string `json:"server_uuid"`
|
||||
GitHubAppUUID string `json:"github_app_uuid"`
|
||||
GitRepository string `json:"git_repository"`
|
||||
GitBranch string `json:"git_branch"`
|
||||
BuildPack string `json:"build_pack"`
|
||||
PortsExposes string `json:"ports_exposes"`
|
||||
|
||||
// Environment (one of these is required)
|
||||
EnvironmentName *string `json:"environment_name,omitempty"`
|
||||
EnvironmentUUID *string `json:"environment,omitempty"`
|
||||
|
||||
// Optional fields (same as public)
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Domains *string `json:"domains,omitempty"`
|
||||
InstantDeploy *bool `json:"instant_deploy,omitempty"`
|
||||
GitCommitSHA *string `json:"git_commit_sha,omitempty"`
|
||||
DestinationUUID *string `json:"destination_uuid,omitempty"`
|
||||
BuildCommand *string `json:"build_command,omitempty"`
|
||||
StartCommand *string `json:"start_command,omitempty"`
|
||||
InstallCommand *string `json:"install_command,omitempty"`
|
||||
BaseDirectory *string `json:"base_directory,omitempty"`
|
||||
PublishDirectory *string `json:"publish_directory,omitempty"`
|
||||
PortsMappings *string `json:"ports_mappings,omitempty"`
|
||||
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"`
|
||||
CustomLabels *string `json:"custom_labels,omitempty"`
|
||||
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"`
|
||||
HealthCheckPath *string `json:"health_check_path,omitempty"`
|
||||
HealthCheckPort *string `json:"health_check_port,omitempty"`
|
||||
HealthCheckMethod *string `json:"health_check_method,omitempty"`
|
||||
LimitsCPUs *string `json:"limits_cpus,omitempty"`
|
||||
LimitsMemory *string `json:"limits_memory,omitempty"`
|
||||
}
|
||||
|
||||
// ApplicationCreateDeployKeyRequest for POST /applications/private-deploy-key
|
||||
// Creates an application from a private repository using SSH deploy key
|
||||
type ApplicationCreateDeployKeyRequest struct {
|
||||
// Required fields
|
||||
ProjectUUID string `json:"project_uuid"`
|
||||
ServerUUID string `json:"server_uuid"`
|
||||
PrivateKeyUUID string `json:"private_key_uuid"`
|
||||
GitRepository string `json:"git_repository"`
|
||||
GitBranch string `json:"git_branch"`
|
||||
BuildPack string `json:"build_pack"`
|
||||
PortsExposes string `json:"ports_exposes"`
|
||||
|
||||
// Environment (one of these is required)
|
||||
EnvironmentName *string `json:"environment_name,omitempty"`
|
||||
EnvironmentUUID *string `json:"environment,omitempty"`
|
||||
|
||||
// Optional fields (same as public)
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Domains *string `json:"domains,omitempty"`
|
||||
InstantDeploy *bool `json:"instant_deploy,omitempty"`
|
||||
GitCommitSHA *string `json:"git_commit_sha,omitempty"`
|
||||
DestinationUUID *string `json:"destination_uuid,omitempty"`
|
||||
BuildCommand *string `json:"build_command,omitempty"`
|
||||
StartCommand *string `json:"start_command,omitempty"`
|
||||
InstallCommand *string `json:"install_command,omitempty"`
|
||||
BaseDirectory *string `json:"base_directory,omitempty"`
|
||||
PublishDirectory *string `json:"publish_directory,omitempty"`
|
||||
PortsMappings *string `json:"ports_mappings,omitempty"`
|
||||
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"`
|
||||
CustomLabels *string `json:"custom_labels,omitempty"`
|
||||
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"`
|
||||
HealthCheckPath *string `json:"health_check_path,omitempty"`
|
||||
HealthCheckPort *string `json:"health_check_port,omitempty"`
|
||||
HealthCheckMethod *string `json:"health_check_method,omitempty"`
|
||||
LimitsCPUs *string `json:"limits_cpus,omitempty"`
|
||||
LimitsMemory *string `json:"limits_memory,omitempty"`
|
||||
}
|
||||
|
||||
// ApplicationCreateDockerfileRequest for POST /applications/dockerfile
|
||||
// Creates an application from a custom Dockerfile
|
||||
type ApplicationCreateDockerfileRequest struct {
|
||||
// Required fields
|
||||
ProjectUUID string `json:"project_uuid"`
|
||||
ServerUUID string `json:"server_uuid"`
|
||||
Dockerfile string `json:"dockerfile"`
|
||||
|
||||
// Environment (one of these is required)
|
||||
EnvironmentName *string `json:"environment_name,omitempty"`
|
||||
EnvironmentUUID *string `json:"environment,omitempty"`
|
||||
|
||||
// Optional fields
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Domains *string `json:"domains,omitempty"`
|
||||
InstantDeploy *bool `json:"instant_deploy,omitempty"`
|
||||
DestinationUUID *string `json:"destination_uuid,omitempty"`
|
||||
PortsExposes *string `json:"ports_exposes,omitempty"`
|
||||
PortsMappings *string `json:"ports_mappings,omitempty"`
|
||||
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"`
|
||||
CustomLabels *string `json:"custom_labels,omitempty"`
|
||||
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"`
|
||||
HealthCheckPath *string `json:"health_check_path,omitempty"`
|
||||
HealthCheckPort *string `json:"health_check_port,omitempty"`
|
||||
HealthCheckMethod *string `json:"health_check_method,omitempty"`
|
||||
LimitsCPUs *string `json:"limits_cpus,omitempty"`
|
||||
LimitsMemory *string `json:"limits_memory,omitempty"`
|
||||
}
|
||||
|
||||
// ApplicationCreateDockerImageRequest for POST /applications/dockerimage
|
||||
// Creates an application from a pre-built Docker image
|
||||
type ApplicationCreateDockerImageRequest struct {
|
||||
// Required fields
|
||||
ProjectUUID string `json:"project_uuid"`
|
||||
ServerUUID string `json:"server_uuid"`
|
||||
DockerRegistryImageName string `json:"docker_registry_image_name"`
|
||||
PortsExposes string `json:"ports_exposes"`
|
||||
|
||||
// Environment (one of these is required)
|
||||
EnvironmentName *string `json:"environment_name,omitempty"`
|
||||
EnvironmentUUID *string `json:"environment,omitempty"`
|
||||
|
||||
// Optional fields
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Domains *string `json:"domains,omitempty"`
|
||||
InstantDeploy *bool `json:"instant_deploy,omitempty"`
|
||||
DestinationUUID *string `json:"destination_uuid,omitempty"`
|
||||
DockerRegistryImageTag *string `json:"docker_registry_image_tag,omitempty"`
|
||||
PortsMappings *string `json:"ports_mappings,omitempty"`
|
||||
CustomDockerRunOptions *string `json:"custom_docker_run_options,omitempty"`
|
||||
CustomLabels *string `json:"custom_labels,omitempty"`
|
||||
HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"`
|
||||
HealthCheckPath *string `json:"health_check_path,omitempty"`
|
||||
HealthCheckPort *string `json:"health_check_port,omitempty"`
|
||||
HealthCheckMethod *string `json:"health_check_method,omitempty"`
|
||||
LimitsCPUs *string `json:"limits_cpus,omitempty"`
|
||||
LimitsMemory *string `json:"limits_memory,omitempty"`
|
||||
}
|
||||
|
||||
@@ -227,3 +227,133 @@ func TestServerCreateRequest_Marshal(t *testing.T) {
|
||||
assert.Equal(t, request.Port, unmarshaled.Port)
|
||||
assert.True(t, unmarshaled.InstantValidate)
|
||||
}
|
||||
|
||||
func TestEnvironmentVariable_IsBuildtimeField(t *testing.T) {
|
||||
// Test that is_buildtime (without underscore) unmarshals correctly
|
||||
jsonData := `{
|
||||
"uuid": "env-123",
|
||||
"key": "TEST_VAR",
|
||||
"value": "test_value",
|
||||
"is_buildtime": true,
|
||||
"is_preview": false,
|
||||
"is_literal": false,
|
||||
"is_shown_once": false,
|
||||
"is_runtime": true,
|
||||
"is_shared": false
|
||||
}`
|
||||
|
||||
var env EnvironmentVariable
|
||||
err := json.Unmarshal([]byte(jsonData), &env)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "env-123", env.UUID)
|
||||
assert.Equal(t, "TEST_VAR", env.Key)
|
||||
assert.True(t, env.IsBuildTime, "is_buildtime should unmarshal to true")
|
||||
assert.True(t, env.IsRuntime, "is_runtime should unmarshal to true")
|
||||
assert.False(t, env.IsShared, "is_shared should unmarshal to false")
|
||||
}
|
||||
|
||||
func TestEnvironmentVariable_MarshalUnmarshal(t *testing.T) {
|
||||
realValue := "secret_value"
|
||||
env := EnvironmentVariable{
|
||||
UUID: "env-uuid-123",
|
||||
Key: "DATABASE_URL",
|
||||
Value: "postgres://localhost/db",
|
||||
IsBuildTime: true,
|
||||
IsPreview: false,
|
||||
IsLiteralValue: true,
|
||||
IsShownOnce: false,
|
||||
IsRuntime: true,
|
||||
IsShared: false,
|
||||
RealValue: &realValue,
|
||||
}
|
||||
|
||||
// Marshal
|
||||
data, err := json.Marshal(env)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify JSON contains is_buildtime (not is_build_time)
|
||||
assert.Contains(t, string(data), `"is_buildtime":true`)
|
||||
assert.NotContains(t, string(data), `"is_build_time"`)
|
||||
|
||||
// Unmarshal
|
||||
var unmarshaled EnvironmentVariable
|
||||
err = json.Unmarshal(data, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, env.UUID, unmarshaled.UUID)
|
||||
assert.Equal(t, env.Key, unmarshaled.Key)
|
||||
assert.Equal(t, env.Value, unmarshaled.Value)
|
||||
assert.True(t, unmarshaled.IsBuildTime)
|
||||
assert.True(t, unmarshaled.IsLiteralValue)
|
||||
assert.True(t, unmarshaled.IsRuntime)
|
||||
assert.False(t, unmarshaled.IsShared)
|
||||
assert.NotNil(t, unmarshaled.RealValue)
|
||||
assert.Equal(t, *env.RealValue, *unmarshaled.RealValue)
|
||||
}
|
||||
|
||||
func TestEnvironmentVariable_UnmarshalFromFixture(t *testing.T) {
|
||||
fixtureData, err := os.ReadFile(filepath.Join("..", "..", "test", "fixtures", "environment_variable_complete.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
var env EnvironmentVariable
|
||||
err = json.Unmarshal(fixtureData, &env)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "env-test-uuid-123", env.UUID)
|
||||
assert.Equal(t, "DATABASE_URL", env.Key)
|
||||
assert.Equal(t, "postgres://localhost/mydb", env.Value)
|
||||
assert.True(t, env.IsBuildTime, "IsBuildTime should be true from fixture")
|
||||
assert.True(t, env.IsRuntime, "IsRuntime should be true from fixture")
|
||||
assert.False(t, env.IsShared, "IsShared should be false from fixture")
|
||||
assert.False(t, env.IsPreview)
|
||||
assert.False(t, env.IsLiteralValue)
|
||||
assert.False(t, env.IsShownOnce)
|
||||
assert.NotNil(t, env.RealValue)
|
||||
assert.Equal(t, "postgres://user:pass@localhost/mydb", *env.RealValue)
|
||||
}
|
||||
|
||||
func TestEnvironmentVariable_PartialResponse(t *testing.T) {
|
||||
// Test backward compatibility with older API responses that might not have all fields
|
||||
jsonData := `{
|
||||
"uuid": "env-123",
|
||||
"key": "OLD_VAR",
|
||||
"value": "old_value"
|
||||
}`
|
||||
|
||||
var env EnvironmentVariable
|
||||
err := json.Unmarshal([]byte(jsonData), &env)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "env-123", env.UUID)
|
||||
assert.Equal(t, "OLD_VAR", env.Key)
|
||||
assert.False(t, env.IsBuildTime, "Missing boolean fields should default to false")
|
||||
assert.False(t, env.IsRuntime, "Missing boolean fields should default to false")
|
||||
assert.False(t, env.IsShared, "Missing boolean fields should default to false")
|
||||
}
|
||||
|
||||
func TestEnvironmentVariableCreateRequest_Marshal(t *testing.T) {
|
||||
isBuildTime := true
|
||||
isPreview := false
|
||||
request := EnvironmentVariableCreateRequest{
|
||||
Key: "NEW_VAR",
|
||||
Value: "new_value",
|
||||
IsBuildTime: &isBuildTime,
|
||||
IsPreview: &isPreview,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Request models should still use is_build_time (with underscore) per API spec
|
||||
assert.Contains(t, string(data), `"is_build_time":true`)
|
||||
|
||||
var unmarshaled EnvironmentVariableCreateRequest
|
||||
err = json.Unmarshal(data, &unmarshaled)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, request.Key, unmarshaled.Key)
|
||||
assert.Equal(t, request.Value, unmarshaled.Value)
|
||||
assert.NotNil(t, unmarshaled.IsBuildTime)
|
||||
assert.True(t, *unmarshaled.IsBuildTime)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ type ServiceCreateRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
ServerUUID string `json:"server_uuid"`
|
||||
ProjectUUID string `json:"project_uuid"`
|
||||
EnvironmentName string `json:"environment_name"`
|
||||
EnvironmentName string `json:"environment_name,omitempty"`
|
||||
EnvironmentUUID *string `json:"environment,omitempty"`
|
||||
InstantDeploy *bool `json:"instant_deploy,omitempty"`
|
||||
DockerCompose *string `json:"docker_compose,omitempty"`
|
||||
Destination *string `json:"destination,omitempty"`
|
||||
@@ -68,3 +69,49 @@ type ServiceUpdateRequest struct {
|
||||
type ServiceLifecycleResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// ServiceEnvironmentVariable represents an environment variable for a service
|
||||
// Services don't have preview deployments, so IsPreview is excluded from output
|
||||
type ServiceEnvironmentVariable 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"`
|
||||
RealValue *string `json:"real_value,omitempty" sensitive:"true"`
|
||||
ServiceID *int `json:"-" table:"-"`
|
||||
CreatedAt string `json:"-" table:"-"`
|
||||
UpdatedAt string `json:"-" table:"-"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// ServiceEnvironmentVariableUpdateRequest represents the request to update a service environment variable
|
||||
type ServiceEnvironmentVariableUpdateRequest struct {
|
||||
Key *string `json:"key,omitempty"`
|
||||
Value *string `json:"value,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// ServiceEnvBulkUpdateRequest represents the request to bulk update service environment variables
|
||||
type ServiceEnvBulkUpdateRequest struct {
|
||||
Data []ServiceEnvironmentVariableCreateRequest `json:"data"`
|
||||
}
|
||||
|
||||
// ServiceEnvBulkUpdateResponse represents the response from service bulk update
|
||||
type ServiceEnvBulkUpdateResponse []ServiceEnvironmentVariable
|
||||
|
||||
@@ -197,3 +197,53 @@ func (s *ApplicationService) BulkUpdateEnvs(ctx context.Context, appUUID string,
|
||||
}
|
||||
return &response, 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
|
||||
err := s.client.Post(ctx, "applications/public", req, &app)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create application from public repository: %w", err)
|
||||
}
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
// CreateGitHubApp creates an application from a private repository using GitHub App
|
||||
func (s *ApplicationService) CreateGitHubApp(ctx context.Context, req *models.ApplicationCreateGitHubAppRequest) (*models.Application, error) {
|
||||
var app models.Application
|
||||
err := s.client.Post(ctx, "applications/private-github-app", req, &app)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create application from private GitHub repository: %w", err)
|
||||
}
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
// CreateDeployKey creates an application from a private repository using SSH deploy key
|
||||
func (s *ApplicationService) CreateDeployKey(ctx context.Context, req *models.ApplicationCreateDeployKeyRequest) (*models.Application, error) {
|
||||
var app models.Application
|
||||
err := s.client.Post(ctx, "applications/private-deploy-key", req, &app)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create application from private repository with deploy key: %w", err)
|
||||
}
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
// CreateDockerfile creates an application from a custom Dockerfile
|
||||
func (s *ApplicationService) CreateDockerfile(ctx context.Context, req *models.ApplicationCreateDockerfileRequest) (*models.Application, error) {
|
||||
var app models.Application
|
||||
err := s.client.Post(ctx, "applications/dockerfile", req, &app)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create application from Dockerfile: %w", err)
|
||||
}
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
// CreateDockerImage creates an application from a pre-built Docker image
|
||||
func (s *ApplicationService) CreateDockerImage(ctx context.Context, req *models.ApplicationCreateDockerImageRequest) (*models.Application, error) {
|
||||
var app models.Application
|
||||
err := s.client.Post(ctx, "applications/dockerimage", req, &app)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create application from Docker image: %w", err)
|
||||
}
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
@@ -737,9 +737,10 @@ func TestApplicationService_UpdateEnv(t *testing.T) {
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
newKey := "API_KEY"
|
||||
newValue := "newsecret456"
|
||||
req := &models.EnvironmentVariableUpdateRequest{
|
||||
UUID: "env-uuid-1",
|
||||
Key: &newKey,
|
||||
Value: &newValue,
|
||||
}
|
||||
|
||||
@@ -760,9 +761,10 @@ func TestApplicationService_UpdateEnv_Error(t *testing.T) {
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
newKey := "API_KEY"
|
||||
newValue := "newsecret456"
|
||||
req := &models.EnvironmentVariableUpdateRequest{
|
||||
UUID: "env-uuid-1",
|
||||
Key: &newKey,
|
||||
Value: &newValue,
|
||||
}
|
||||
|
||||
@@ -801,3 +803,396 @@ func TestApplicationService_DeleteEnv_Error(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to delete environment variable")
|
||||
}
|
||||
|
||||
func TestApplicationService_ListEnvs_AllFields(t *testing.T) {
|
||||
// Test that all fields including is_buildtime (without underscore), is_runtime, and is_shared
|
||||
// are correctly parsed from API response
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs", r.URL.Path)
|
||||
assert.Equal(t, "GET", r.Method)
|
||||
|
||||
// Mock API response with all fields
|
||||
envs := []models.EnvironmentVariable{
|
||||
{
|
||||
UUID: "env-1",
|
||||
Key: "DATABASE_URL",
|
||||
Value: "postgres://localhost",
|
||||
IsBuildTime: true,
|
||||
IsPreview: false,
|
||||
IsLiteralValue: true,
|
||||
IsShownOnce: false,
|
||||
IsRuntime: true,
|
||||
IsShared: false,
|
||||
},
|
||||
{
|
||||
UUID: "env-2",
|
||||
Key: "API_KEY",
|
||||
Value: "secret",
|
||||
IsBuildTime: false,
|
||||
IsPreview: true,
|
||||
IsLiteralValue: false,
|
||||
IsShownOnce: false,
|
||||
IsRuntime: false,
|
||||
IsShared: true,
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(envs)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
result, err := svc.ListEnvs(context.Background(), "app-uuid-123")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result, 2)
|
||||
|
||||
// Verify first env var
|
||||
assert.Equal(t, "DATABASE_URL", result[0].Key)
|
||||
assert.True(t, result[0].IsBuildTime, "IsBuildTime should be true for DATABASE_URL")
|
||||
assert.True(t, result[0].IsRuntime, "IsRuntime should be true for DATABASE_URL")
|
||||
assert.False(t, result[0].IsShared, "IsShared should be false for DATABASE_URL")
|
||||
assert.True(t, result[0].IsLiteralValue)
|
||||
assert.False(t, result[0].IsPreview)
|
||||
|
||||
// Verify second env var
|
||||
assert.Equal(t, "API_KEY", result[1].Key)
|
||||
assert.False(t, result[1].IsBuildTime, "IsBuildTime should be false for API_KEY")
|
||||
assert.False(t, result[1].IsRuntime, "IsRuntime should be false for API_KEY")
|
||||
assert.True(t, result[1].IsShared, "IsShared should be true for API_KEY")
|
||||
assert.False(t, result[1].IsLiteralValue)
|
||||
assert.True(t, result[1].IsPreview)
|
||||
}
|
||||
|
||||
func TestApplicationService_EnvBuildtimeFlag(t *testing.T) {
|
||||
// Test specifically that is_buildtime (without underscore) unmarshals correctly
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs", r.URL.Path)
|
||||
|
||||
// Directly write JSON with is_buildtime (no underscore) to mimic actual API
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`[
|
||||
{
|
||||
"uuid": "env-test-1",
|
||||
"key": "BUILD_VAR",
|
||||
"value": "build_value",
|
||||
"is_buildtime": true,
|
||||
"is_preview": false,
|
||||
"is_literal": false,
|
||||
"is_shown_once": false,
|
||||
"is_runtime": true,
|
||||
"is_shared": false
|
||||
}
|
||||
]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
result, err := svc.ListEnvs(context.Background(), "app-uuid-123")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result, 1)
|
||||
assert.Equal(t, "BUILD_VAR", result[0].Key)
|
||||
assert.True(t, result[0].IsBuildTime, "is_buildtime field should unmarshal correctly to true")
|
||||
assert.True(t, result[0].IsRuntime, "is_runtime field should unmarshal correctly to true")
|
||||
}
|
||||
|
||||
func TestApplicationService_EnvRuntimeAndShared(t *testing.T) {
|
||||
// Test is_runtime and is_shared fields specifically
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/applications/app-uuid-123/envs", r.URL.Path)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`[
|
||||
{
|
||||
"uuid": "env-runtime",
|
||||
"key": "RUNTIME_VAR",
|
||||
"value": "runtime_value",
|
||||
"is_buildtime": false,
|
||||
"is_preview": false,
|
||||
"is_literal": false,
|
||||
"is_shown_once": false,
|
||||
"is_runtime": true,
|
||||
"is_shared": false
|
||||
},
|
||||
{
|
||||
"uuid": "env-shared",
|
||||
"key": "SHARED_VAR",
|
||||
"value": "shared_value",
|
||||
"is_buildtime": false,
|
||||
"is_preview": false,
|
||||
"is_literal": false,
|
||||
"is_shown_once": false,
|
||||
"is_runtime": false,
|
||||
"is_shared": true
|
||||
}
|
||||
]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
result, err := svc.ListEnvs(context.Background(), "app-uuid-123")
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result, 2)
|
||||
|
||||
// Verify runtime var
|
||||
assert.Equal(t, "RUNTIME_VAR", result[0].Key)
|
||||
assert.True(t, result[0].IsRuntime, "IsRuntime should be true")
|
||||
assert.False(t, result[0].IsShared, "IsShared should be false")
|
||||
|
||||
// Verify shared var
|
||||
assert.Equal(t, "SHARED_VAR", result[1].Key)
|
||||
assert.False(t, result[1].IsRuntime, "IsRuntime should be false")
|
||||
assert.True(t, result[1].IsShared, "IsShared should be true")
|
||||
}
|
||||
|
||||
func TestApplicationService_CreatePublic(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/applications/public", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
|
||||
|
||||
var req models.ApplicationCreatePublicRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "proj-uuid", req.ProjectUUID)
|
||||
assert.Equal(t, "server-uuid", req.ServerUUID)
|
||||
assert.Equal(t, "https://github.com/user/repo", req.GitRepository)
|
||||
assert.Equal(t, "main", req.GitBranch)
|
||||
assert.Equal(t, "nixpacks", req.BuildPack)
|
||||
assert.Equal(t, "3000", req.PortsExposes)
|
||||
|
||||
branch := "main"
|
||||
fqdn := "app.example.com"
|
||||
app := models.Application{
|
||||
UUID: "new-app-uuid",
|
||||
Name: "My App",
|
||||
Status: "starting",
|
||||
GitBranch: &branch,
|
||||
FQDN: &fqdn,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(app)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
envName := "production"
|
||||
req := &models.ApplicationCreatePublicRequest{
|
||||
ProjectUUID: "proj-uuid",
|
||||
ServerUUID: "server-uuid",
|
||||
GitRepository: "https://github.com/user/repo",
|
||||
GitBranch: "main",
|
||||
BuildPack: "nixpacks",
|
||||
PortsExposes: "3000",
|
||||
EnvironmentName: &envName,
|
||||
}
|
||||
|
||||
result, err := svc.CreatePublic(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "new-app-uuid", result.UUID)
|
||||
assert.Equal(t, "My App", result.Name)
|
||||
}
|
||||
|
||||
func TestApplicationService_CreatePublic_Error(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`{"message":"invalid repository URL"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
envName := "production"
|
||||
req := &models.ApplicationCreatePublicRequest{
|
||||
ProjectUUID: "proj-uuid",
|
||||
ServerUUID: "server-uuid",
|
||||
GitRepository: "invalid-repo",
|
||||
GitBranch: "main",
|
||||
BuildPack: "nixpacks",
|
||||
PortsExposes: "3000",
|
||||
EnvironmentName: &envName,
|
||||
}
|
||||
|
||||
result, err := svc.CreatePublic(context.Background(), req)
|
||||
require.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
assert.Contains(t, err.Error(), "failed to create application from public repository")
|
||||
}
|
||||
|
||||
func TestApplicationService_CreateGitHubApp(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/applications/private-github-app", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
var req models.ApplicationCreateGitHubAppRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "github-app-uuid", req.GitHubAppUUID)
|
||||
assert.Equal(t, "owner/repo", req.GitRepository)
|
||||
|
||||
branch := "main"
|
||||
app := models.Application{
|
||||
UUID: "new-app-uuid",
|
||||
Name: "Private App",
|
||||
Status: "starting",
|
||||
GitBranch: &branch,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(app)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
envName := "production"
|
||||
req := &models.ApplicationCreateGitHubAppRequest{
|
||||
ProjectUUID: "proj-uuid",
|
||||
ServerUUID: "server-uuid",
|
||||
GitHubAppUUID: "github-app-uuid",
|
||||
GitRepository: "owner/repo",
|
||||
GitBranch: "main",
|
||||
BuildPack: "nixpacks",
|
||||
PortsExposes: "3000",
|
||||
EnvironmentName: &envName,
|
||||
}
|
||||
|
||||
result, err := svc.CreateGitHubApp(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "new-app-uuid", result.UUID)
|
||||
}
|
||||
|
||||
func TestApplicationService_CreateDeployKey(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/applications/private-deploy-key", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
var req models.ApplicationCreateDeployKeyRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "key-uuid", req.PrivateKeyUUID)
|
||||
assert.Equal(t, "git@github.com:owner/repo.git", req.GitRepository)
|
||||
|
||||
branch := "main"
|
||||
app := models.Application{
|
||||
UUID: "new-app-uuid",
|
||||
Name: "Deploy Key App",
|
||||
Status: "starting",
|
||||
GitBranch: &branch,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(app)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
envName := "production"
|
||||
req := &models.ApplicationCreateDeployKeyRequest{
|
||||
ProjectUUID: "proj-uuid",
|
||||
ServerUUID: "server-uuid",
|
||||
PrivateKeyUUID: "key-uuid",
|
||||
GitRepository: "git@github.com:owner/repo.git",
|
||||
GitBranch: "main",
|
||||
BuildPack: "nixpacks",
|
||||
PortsExposes: "3000",
|
||||
EnvironmentName: &envName,
|
||||
}
|
||||
|
||||
result, err := svc.CreateDeployKey(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "new-app-uuid", result.UUID)
|
||||
}
|
||||
|
||||
func TestApplicationService_CreateDockerfile(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/applications/dockerfile", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
var req models.ApplicationCreateDockerfileRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, req.Dockerfile, "FROM node:18")
|
||||
|
||||
app := models.Application{
|
||||
UUID: "new-app-uuid",
|
||||
Name: "Dockerfile App",
|
||||
Status: "starting",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(app)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
envName := "production"
|
||||
req := &models.ApplicationCreateDockerfileRequest{
|
||||
ProjectUUID: "proj-uuid",
|
||||
ServerUUID: "server-uuid",
|
||||
Dockerfile: "FROM node:18\nCOPY . .\nCMD [\"node\", \"app.js\"]",
|
||||
EnvironmentName: &envName,
|
||||
}
|
||||
|
||||
result, err := svc.CreateDockerfile(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "new-app-uuid", result.UUID)
|
||||
}
|
||||
|
||||
func TestApplicationService_CreateDockerImage(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/applications/dockerimage", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
var req models.ApplicationCreateDockerImageRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nginx:latest", req.DockerRegistryImageName)
|
||||
assert.Equal(t, "80", req.PortsExposes)
|
||||
|
||||
app := models.Application{
|
||||
UUID: "new-app-uuid",
|
||||
Name: "Docker Image App",
|
||||
Status: "starting",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(app)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewApplicationService(client)
|
||||
|
||||
envName := "production"
|
||||
req := &models.ApplicationCreateDockerImageRequest{
|
||||
ProjectUUID: "proj-uuid",
|
||||
ServerUUID: "server-uuid",
|
||||
DockerRegistryImageName: "nginx:latest",
|
||||
PortsExposes: "80",
|
||||
EnvironmentName: &envName,
|
||||
}
|
||||
|
||||
result, err := svc.CreateDockerImage(context.Background(), req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "new-app-uuid", result.UUID)
|
||||
}
|
||||
|
||||
@@ -39,3 +39,13 @@ func (s *ProjectService) Get(ctx context.Context, uuid string) (*models.Project,
|
||||
}
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
// Create creates a new project
|
||||
func (s *ProjectService) Create(ctx context.Context, req *models.ProjectCreateRequest) (*models.Project, error) {
|
||||
var project models.Project
|
||||
err := s.client.Post(ctx, "projects", req, &project)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create project: %w", err)
|
||||
}
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
@@ -74,3 +74,71 @@ func TestProjectService_Get(t *testing.T) {
|
||||
assert.Equal(t, "proj-1", result.UUID)
|
||||
assert.Equal(t, "Test Project", result.Name)
|
||||
}
|
||||
|
||||
func TestProjectService_Create(t *testing.T) {
|
||||
desc := "New Project Description"
|
||||
project := models.Project{
|
||||
UUID: "proj-new",
|
||||
Name: "New Project",
|
||||
Description: &desc,
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/projects", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
var req models.ProjectCreateRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "New Project", req.Name)
|
||||
assert.NotNil(t, req.Description)
|
||||
assert.Equal(t, "New Project Description", *req.Description)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(project)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewProjectService(client)
|
||||
|
||||
result, err := svc.Create(context.Background(), &models.ProjectCreateRequest{
|
||||
Name: "New Project",
|
||||
Description: &desc,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "proj-new", result.UUID)
|
||||
assert.Equal(t, "New Project", result.Name)
|
||||
}
|
||||
|
||||
func TestProjectService_Create_NameOnly(t *testing.T) {
|
||||
project := models.Project{
|
||||
UUID: "proj-minimal",
|
||||
Name: "Minimal Project",
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/projects", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
var req models.ProjectCreateRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Minimal Project", req.Name)
|
||||
assert.Nil(t, req.Description)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(project)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewProjectService(client)
|
||||
|
||||
result, err := svc.Create(context.Background(), &models.ProjectCreateRequest{
|
||||
Name: "Minimal Project",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "proj-minimal", result.UUID)
|
||||
assert.Equal(t, "Minimal Project", result.Name)
|
||||
}
|
||||
|
||||
+10
-10
@@ -101,8 +101,8 @@ func (s *Service) Restart(ctx context.Context, uuid string) (*models.ServiceLife
|
||||
}
|
||||
|
||||
// ListEnvs retrieves all environment variables for a service
|
||||
func (s *Service) ListEnvs(ctx context.Context, uuid string) ([]models.EnvironmentVariable, error) {
|
||||
var envs []models.EnvironmentVariable
|
||||
func (s *Service) ListEnvs(ctx context.Context, uuid string) ([]models.ServiceEnvironmentVariable, error) {
|
||||
var envs []models.ServiceEnvironmentVariable
|
||||
err := s.client.Get(ctx, fmt.Sprintf("services/%s/envs", uuid), &envs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list environment variables for service %s: %w", uuid, err)
|
||||
@@ -111,7 +111,7 @@ func (s *Service) ListEnvs(ctx context.Context, uuid string) ([]models.Environme
|
||||
}
|
||||
|
||||
// GetEnv retrieves a single environment variable by UUID or key
|
||||
func (s *Service) GetEnv(ctx context.Context, serviceUUID, envIdentifier string) (*models.EnvironmentVariable, error) {
|
||||
func (s *Service) GetEnv(ctx context.Context, serviceUUID, envIdentifier string) (*models.ServiceEnvironmentVariable, error) {
|
||||
envs, err := s.ListEnvs(ctx, serviceUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -128,8 +128,8 @@ func (s *Service) GetEnv(ctx context.Context, serviceUUID, envIdentifier string)
|
||||
}
|
||||
|
||||
// CreateEnv creates a new environment variable for a service
|
||||
func (s *Service) CreateEnv(ctx context.Context, uuid string, req *models.EnvironmentVariableCreateRequest) (*models.EnvironmentVariable, error) {
|
||||
var env models.EnvironmentVariable
|
||||
func (s *Service) CreateEnv(ctx context.Context, uuid string, req *models.ServiceEnvironmentVariableCreateRequest) (*models.ServiceEnvironmentVariable, error) {
|
||||
var env models.ServiceEnvironmentVariable
|
||||
err := s.client.Post(ctx, fmt.Sprintf("services/%s/envs", uuid), req, &env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create environment variable for service %s: %w", uuid, err)
|
||||
@@ -138,8 +138,8 @@ func (s *Service) CreateEnv(ctx context.Context, uuid string, req *models.Enviro
|
||||
}
|
||||
|
||||
// UpdateEnv updates an environment variable for a service
|
||||
func (s *Service) UpdateEnv(ctx context.Context, serviceUUID string, req *models.EnvironmentVariableUpdateRequest) (*models.EnvironmentVariable, error) {
|
||||
var env models.EnvironmentVariable
|
||||
func (s *Service) UpdateEnv(ctx context.Context, serviceUUID string, req *models.ServiceEnvironmentVariableUpdateRequest) (*models.ServiceEnvironmentVariable, error) {
|
||||
var env models.ServiceEnvironmentVariable
|
||||
err := s.client.Patch(ctx, fmt.Sprintf("services/%s/envs", serviceUUID), req, &env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update environment variable for service %s: %w", serviceUUID, err)
|
||||
@@ -157,11 +157,11 @@ func (s *Service) DeleteEnv(ctx context.Context, serviceUUID, envUUID string) er
|
||||
}
|
||||
|
||||
// BulkUpdateEnvs updates multiple environment variables in a single request
|
||||
func (s *Service) BulkUpdateEnvs(ctx context.Context, serviceUUID string, req *BulkUpdateEnvsRequest) (*BulkUpdateEnvsResponse, error) {
|
||||
var response BulkUpdateEnvsResponse
|
||||
func (s *Service) BulkUpdateEnvs(ctx context.Context, serviceUUID string, req *models.ServiceEnvBulkUpdateRequest) (models.ServiceEnvBulkUpdateResponse, error) {
|
||||
var response models.ServiceEnvBulkUpdateResponse
|
||||
err := s.client.Patch(ctx, fmt.Sprintf("services/%s/envs/bulk", serviceUUID), req, &response)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to bulk update environment variables for service %s: %w", serviceUUID, err)
|
||||
}
|
||||
return &response, nil
|
||||
return response, nil
|
||||
}
|
||||
|
||||
@@ -255,14 +255,14 @@ func TestService_ListEnvs(t *testing.T) {
|
||||
"uuid": "env-1",
|
||||
"key": "DATABASE_URL",
|
||||
"value": "postgres://localhost",
|
||||
"is_build_time": false,
|
||||
"is_buildtime": false,
|
||||
"is_preview": false
|
||||
},
|
||||
{
|
||||
"uuid": "env-2",
|
||||
"key": "API_KEY",
|
||||
"value": "secret",
|
||||
"is_build_time": true,
|
||||
"is_buildtime": true,
|
||||
"is_preview": false
|
||||
}
|
||||
]`))
|
||||
@@ -290,7 +290,7 @@ func TestService_CreateEnv(t *testing.T) {
|
||||
"uuid": "env-new",
|
||||
"key": "NEW_VAR",
|
||||
"value": "new_value",
|
||||
"is_build_time": false,
|
||||
"is_buildtime": false,
|
||||
"is_preview": false
|
||||
}`))
|
||||
}))
|
||||
@@ -299,7 +299,7 @@ func TestService_CreateEnv(t *testing.T) {
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewService(client)
|
||||
|
||||
env, err := svc.CreateEnv(context.Background(), "service-uuid-123", &models.EnvironmentVariableCreateRequest{
|
||||
env, err := svc.CreateEnv(context.Background(), "service-uuid-123", &models.ServiceEnvironmentVariableCreateRequest{
|
||||
Key: "NEW_VAR",
|
||||
Value: "new_value",
|
||||
})
|
||||
@@ -319,7 +319,7 @@ func TestService_UpdateEnv(t *testing.T) {
|
||||
"uuid": "env-123",
|
||||
"key": "UPDATED_VAR",
|
||||
"value": "updated_value",
|
||||
"is_build_time": true,
|
||||
"is_buildtime": true,
|
||||
"is_preview": false
|
||||
}`))
|
||||
}))
|
||||
@@ -329,9 +329,8 @@ func TestService_UpdateEnv(t *testing.T) {
|
||||
svc := NewService(client)
|
||||
|
||||
newKey := "UPDATED_VAR"
|
||||
env, err := svc.UpdateEnv(context.Background(), "service-uuid-123", &models.EnvironmentVariableUpdateRequest{
|
||||
UUID: "env-123",
|
||||
Key: &newKey,
|
||||
env, err := svc.UpdateEnv(context.Background(), "service-uuid-123", &models.ServiceEnvironmentVariableUpdateRequest{
|
||||
Key: &newKey,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
@@ -354,3 +353,85 @@ func TestService_DeleteEnv(t *testing.T) {
|
||||
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestService_Create(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/services", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write([]byte(`{
|
||||
"uuid": "service-new-uuid",
|
||||
"name": "WordPress",
|
||||
"status": "starting"
|
||||
}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewService(client)
|
||||
|
||||
name := "My WordPress"
|
||||
service, err := svc.Create(context.Background(), &models.ServiceCreateRequest{
|
||||
Type: "wordpress-with-mysql",
|
||||
ServerUUID: "server-uuid",
|
||||
ProjectUUID: "project-uuid",
|
||||
EnvironmentName: "production",
|
||||
Name: &name,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "service-new-uuid", service.UUID)
|
||||
assert.Equal(t, "WordPress", service.Name)
|
||||
}
|
||||
|
||||
func TestService_Create_WithInstantDeploy(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/v1/services", r.URL.Path)
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write([]byte(`{
|
||||
"uuid": "service-instant-uuid",
|
||||
"name": "Ghost Blog",
|
||||
"status": "running"
|
||||
}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewService(client)
|
||||
|
||||
instantDeploy := true
|
||||
service, err := svc.Create(context.Background(), &models.ServiceCreateRequest{
|
||||
Type: "ghost",
|
||||
ServerUUID: "server-uuid",
|
||||
ProjectUUID: "project-uuid",
|
||||
EnvironmentName: "production",
|
||||
InstantDeploy: &instantDeploy,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "service-instant-uuid", service.UUID)
|
||||
assert.Equal(t, "Ghost Blog", service.Name)
|
||||
}
|
||||
|
||||
func TestService_Create_Error(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte(`{"error": "invalid service type"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := api.NewClient(server.URL, "test-token")
|
||||
svc := NewService(client)
|
||||
|
||||
_, err := svc.Create(context.Background(), &models.ServiceCreateRequest{
|
||||
Type: "invalid-type",
|
||||
ServerUUID: "server-uuid",
|
||||
ProjectUUID: "project-uuid",
|
||||
EnvironmentName: "production",
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
+41
-43
@@ -5,92 +5,90 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
compareVersion "github.com/hashicorp/go-version"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Version variables injected by GoReleaser at build time via ldflags
|
||||
var (
|
||||
version = "v1.0.3"
|
||||
version = "v1.4.0"
|
||||
)
|
||||
|
||||
// GitHubAPIURL is the URL for fetching CLI version tags (exported for testing)
|
||||
var GitHubAPIURL = "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
|
||||
|
||||
func GetVersion() string {
|
||||
return version
|
||||
}
|
||||
|
||||
// CheckInterval for version checking
|
||||
const CheckInterval = 10 * time.Minute
|
||||
|
||||
// Tag represents a git tag for version checking
|
||||
type Tag struct {
|
||||
Ref string `json:"ref"`
|
||||
}
|
||||
|
||||
// CheckLatestVersionOfCli checks for CLI updates
|
||||
func CheckLatestVersionOfCli(debug bool) (string, error) {
|
||||
lastCheck := viper.GetString("lastupdatechecktime")
|
||||
if lastCheck != "" {
|
||||
lastCheckTime, err := time.Parse(time.RFC3339, lastCheck)
|
||||
if err == nil && lastCheckTime.Add(CheckInterval).After(time.Now()) {
|
||||
if debug {
|
||||
log.Println("Skipping update check. Last check was less than 10 minutes ago.")
|
||||
}
|
||||
return GetVersion(), nil
|
||||
}
|
||||
}
|
||||
// CheckLatestVersionOfCli checks for CLI updates on every command.
|
||||
// Errors are handled silently - the function returns without printing anything
|
||||
// if the GitHub API call fails.
|
||||
func CheckLatestVersionOfCli(_ bool) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Update check time
|
||||
viper.Set("lastupdatechecktime", time.Now().Format(time.RFC3339))
|
||||
if err := viper.WriteConfig(); err != nil {
|
||||
log.Printf("Failed to write config: %v\n", err)
|
||||
}
|
||||
|
||||
url := "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
|
||||
ctx := context.Background()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", GitHubAPIURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil // Silent fail
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil // Silent fail
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
if resp.StatusCode != 200 {
|
||||
return "", nil // Silent fail
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("%d - Failed to fetch data from %s. Error: %s", resp.StatusCode, url, string(body))
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", nil // Silent fail
|
||||
}
|
||||
|
||||
var tags []Tag
|
||||
if err := json.Unmarshal(body, &tags); err != nil {
|
||||
return "", err
|
||||
return "", nil // Silent fail
|
||||
}
|
||||
|
||||
if len(tags) == 0 {
|
||||
return "", nil // Silent fail
|
||||
}
|
||||
|
||||
versionsRaw := make([]string, 0, len(tags))
|
||||
for _, tag := range tags {
|
||||
versionStr := tag.Ref[10:]
|
||||
versionsRaw = append(versionsRaw, versionStr)
|
||||
if len(tag.Ref) > 10 {
|
||||
versionStr := tag.Ref[10:]
|
||||
versionsRaw = append(versionsRaw, versionStr)
|
||||
}
|
||||
}
|
||||
|
||||
versions := make([]*compareVersion.Version, len(versionsRaw))
|
||||
for i, raw := range versionsRaw {
|
||||
if len(versionsRaw) == 0 {
|
||||
return "", nil // Silent fail
|
||||
}
|
||||
|
||||
versions := make([]*compareVersion.Version, 0, len(versionsRaw))
|
||||
for _, raw := range versionsRaw {
|
||||
v, err := compareVersion.NewVersion(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
continue // Skip invalid versions
|
||||
}
|
||||
versions[i] = v
|
||||
versions = append(versions, v)
|
||||
}
|
||||
|
||||
if len(versions) == 0 {
|
||||
return "", nil // Silent fail
|
||||
}
|
||||
|
||||
sort.Sort(compareVersion.Collection(versions))
|
||||
@@ -99,11 +97,11 @@ func CheckLatestVersionOfCli(debug bool) (string, error) {
|
||||
// Compare versions properly using semantic versioning
|
||||
currentVersion, err := compareVersion.NewVersion(GetVersion())
|
||||
if err != nil {
|
||||
return latestVersion.String(), err
|
||||
return "", nil // Silent fail
|
||||
}
|
||||
|
||||
if latestVersion.GreaterThan(currentVersion) {
|
||||
fmt.Printf("There is a new version of Coolify CLI available.\nPlease update with 'coolify update'.\n\n")
|
||||
fmt.Printf("A new version (%s) is available. Update with: coolify update\n", latestVersion.String())
|
||||
}
|
||||
return latestVersion.String(), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
v := GetVersion()
|
||||
if v == "" {
|
||||
t.Error("GetVersion() returned empty string")
|
||||
}
|
||||
// Version should start with 'v'
|
||||
if v[0] != 'v' {
|
||||
t.Errorf("GetVersion() = %q, expected to start with 'v'", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLatestVersionOfCli_UpdateAvailable(t *testing.T) {
|
||||
// Save original values
|
||||
originalURL := GitHubAPIURL
|
||||
originalVersion := version
|
||||
defer func() {
|
||||
GitHubAPIURL = originalURL
|
||||
version = originalVersion
|
||||
}()
|
||||
|
||||
// Set a low version to ensure update is available
|
||||
version = "v0.0.1"
|
||||
|
||||
// Create mock server with newer version
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// Return tags in GitHub API format
|
||||
_, _ = w.Write([]byte(`[{"ref":"refs/tags/v1.0.0"},{"ref":"refs/tags/v2.0.0"}]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
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()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
if latestVersion != "2.0.0" {
|
||||
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want %q", latestVersion, "2.0.0")
|
||||
}
|
||||
|
||||
// Should print update message
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLatestVersionOfCli_NoUpdate(t *testing.T) {
|
||||
// Save original values
|
||||
originalURL := GitHubAPIURL
|
||||
originalVersion := version
|
||||
defer func() {
|
||||
GitHubAPIURL = originalURL
|
||||
version = originalVersion
|
||||
}()
|
||||
|
||||
// Set a high version to ensure no update is available
|
||||
version = "v99.99.99"
|
||||
|
||||
// Create mock server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`[{"ref":"refs/tags/v1.0.0"},{"ref":"refs/tags/v2.0.0"}]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
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()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
// Function returns the latest version from GitHub (2.0.0), not the current version
|
||||
if latestVersion != "2.0.0" {
|
||||
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want %q", latestVersion, "2.0.0")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLatestVersionOfCli_APIError_SilentFail(t *testing.T) {
|
||||
// Save original URL
|
||||
originalURL := GitHubAPIURL
|
||||
defer func() {
|
||||
GitHubAPIURL = originalURL
|
||||
}()
|
||||
|
||||
// Create mock server that returns error
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(`{"error": "internal server error"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
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()
|
||||
|
||||
// Should return empty string and nil error (silent fail)
|
||||
if err != nil {
|
||||
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on API error", err)
|
||||
}
|
||||
|
||||
if latestVersion != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on API error", latestVersion)
|
||||
}
|
||||
|
||||
// Should NOT print anything on error
|
||||
if output != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not print anything on API error, got: %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLatestVersionOfCli_NetworkError_SilentFail(t *testing.T) {
|
||||
// Save original URL
|
||||
originalURL := GitHubAPIURL
|
||||
defer func() {
|
||||
GitHubAPIURL = originalURL
|
||||
}()
|
||||
|
||||
// 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()
|
||||
|
||||
// Should return empty string and nil error (silent fail)
|
||||
if err != nil {
|
||||
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on network error", err)
|
||||
}
|
||||
|
||||
if latestVersion != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on network error", latestVersion)
|
||||
}
|
||||
|
||||
// Should NOT print anything on error
|
||||
if output != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() should not print anything on network error, got: %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLatestVersionOfCli_InvalidJSON_SilentFail(t *testing.T) {
|
||||
// Save original URL
|
||||
originalURL := GitHubAPIURL
|
||||
defer func() {
|
||||
GitHubAPIURL = originalURL
|
||||
}()
|
||||
|
||||
// Create mock server that returns invalid JSON
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`not valid json`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
GitHubAPIURL = server.URL
|
||||
|
||||
latestVersion, err := CheckLatestVersionOfCli(false)
|
||||
|
||||
// Should return empty string and nil error (silent fail)
|
||||
if err != nil {
|
||||
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on invalid JSON", err)
|
||||
}
|
||||
|
||||
if latestVersion != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on invalid JSON", latestVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckLatestVersionOfCli_EmptyTags_SilentFail(t *testing.T) {
|
||||
// Save original URL
|
||||
originalURL := GitHubAPIURL
|
||||
defer func() {
|
||||
GitHubAPIURL = originalURL
|
||||
}()
|
||||
|
||||
// Create mock server that returns empty array
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
GitHubAPIURL = server.URL
|
||||
|
||||
latestVersion, err := CheckLatestVersionOfCli(false)
|
||||
|
||||
// Should return empty string and nil error (silent fail)
|
||||
if err != nil {
|
||||
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on empty tags", err)
|
||||
}
|
||||
|
||||
if latestVersion != "" {
|
||||
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on empty tags", latestVersion)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
# Coolify CLI Installer for Windows
|
||||
# This script installs the coolify-cli from GitHub releases
|
||||
# Supports Windows on amd64/arm64 architectures
|
||||
|
||||
#Requires -Version 5.1
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Position = 0)]
|
||||
[string]$Version = "",
|
||||
|
||||
[Parameter()]
|
||||
[switch]$User,
|
||||
|
||||
[Parameter()]
|
||||
[switch]$Help,
|
||||
|
||||
[Parameter()]
|
||||
[string]$InstallDir = ""
|
||||
)
|
||||
|
||||
# Support environment variables for web-based installation
|
||||
if (-not $Version -and $env:COOLIFY_VERSION) {
|
||||
$Version = $env:COOLIFY_VERSION
|
||||
}
|
||||
if (-not $User.IsPresent -and $env:COOLIFY_USER_INSTALL) {
|
||||
$User = [bool]($env:COOLIFY_USER_INSTALL -match '^(1|true|yes)$')
|
||||
}
|
||||
if (-not $InstallDir -and $env:COOLIFY_INSTALL_DIR) {
|
||||
$InstallDir = $env:COOLIFY_INSTALL_DIR
|
||||
}
|
||||
|
||||
# Configuration
|
||||
$Script:REPO = "coollabsio/coolify-cli"
|
||||
$Script:BINARY_NAME = "coolify.exe"
|
||||
$Script:GLOBAL_INSTALL_DIR = "$env:ProgramFiles\Coolify"
|
||||
$Script:USER_INSTALL_DIR = "$env:LOCALAPPDATA\Coolify"
|
||||
$Script:TEMP_FILE = ""
|
||||
|
||||
# Error action preference
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Cleanup function
|
||||
function Cleanup {
|
||||
if ($Script:TEMP_FILE -and (Test-Path $Script:TEMP_FILE)) {
|
||||
Remove-Item -Path $Script:TEMP_FILE -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Register cleanup on exit
|
||||
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { Cleanup } | Out-Null
|
||||
|
||||
# Show help
|
||||
function Show-Help {
|
||||
Write-Host @"
|
||||
Coolify CLI Installer for Windows
|
||||
|
||||
Usage: .\install.ps1 [OPTIONS] [VERSION]
|
||||
|
||||
OPTIONS:
|
||||
-User Install to user directory (no admin rights required)
|
||||
-InstallDir Custom installation directory
|
||||
-Help Show this help message
|
||||
|
||||
ARGUMENTS:
|
||||
VERSION Specific version to install (e.g., v1.0.0)
|
||||
If not specified, installs the latest release
|
||||
|
||||
EXAMPLES:
|
||||
.\install.ps1 Install latest version to Program Files
|
||||
.\install.ps1 -User Install latest version to user directory
|
||||
.\install.ps1 v1.0.0 Install specific version to Program Files
|
||||
.\install.ps1 -User v1.0.0 Install specific version to user directory
|
||||
.\install.ps1 -InstallDir "C:\Tools" Install to custom directory
|
||||
|
||||
NOTES:
|
||||
- Administrator privileges are required for global installation
|
||||
- User installation does not require admin rights
|
||||
- The installation directory will be added to PATH if not already present
|
||||
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Write colored output
|
||||
function Write-ColorOutput {
|
||||
param(
|
||||
[string]$Message,
|
||||
[string]$ForegroundColor = "White"
|
||||
)
|
||||
Write-Host $Message -ForegroundColor $ForegroundColor
|
||||
}
|
||||
|
||||
function Write-Success {
|
||||
param([string]$Message)
|
||||
Write-ColorOutput $Message -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Warning {
|
||||
param([string]$Message)
|
||||
Write-ColorOutput $Message -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Write-ErrorMessage {
|
||||
param([string]$Message)
|
||||
Write-ColorOutput $Message -ForegroundColor Red
|
||||
}
|
||||
|
||||
# Error handler
|
||||
function Stop-WithError {
|
||||
param([string]$Message)
|
||||
Write-ErrorMessage "Error: $Message"
|
||||
Cleanup
|
||||
throw $Message
|
||||
}
|
||||
|
||||
# Check if running as administrator
|
||||
function Test-Administrator {
|
||||
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
# Detect platform and architecture
|
||||
function Get-PlatformInfo {
|
||||
$os = "windows"
|
||||
|
||||
# Detect architecture
|
||||
$arch = switch ($env:PROCESSOR_ARCHITECTURE) {
|
||||
"AMD64" { "amd64" }
|
||||
"ARM64" { "arm64" }
|
||||
"x86" { Stop-WithError "32-bit Windows is not supported" }
|
||||
default { Stop-WithError "Unsupported architecture: $env:PROCESSOR_ARCHITECTURE" }
|
||||
}
|
||||
|
||||
return @{
|
||||
OS = $os
|
||||
Arch = $arch
|
||||
}
|
||||
}
|
||||
|
||||
# Fetch latest release version from GitHub
|
||||
function Get-LatestVersion {
|
||||
Write-Host "Fetching latest release version..."
|
||||
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri "https://api.github.com/repos/$Script:REPO/releases/latest" -Method Get
|
||||
$latestVersion = $response.tag_name
|
||||
|
||||
if (-not $latestVersion) {
|
||||
Stop-WithError "Failed to fetch latest release version from GitHub"
|
||||
}
|
||||
|
||||
return $latestVersion
|
||||
}
|
||||
catch {
|
||||
Stop-WithError "Failed to fetch latest release version: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Validate version format
|
||||
function Test-VersionFormat {
|
||||
param([string]$VersionString)
|
||||
|
||||
# Check if version matches semantic versioning (with or without 'v' prefix)
|
||||
if ($VersionString -notmatch '^v?\d+\.\d+\.\d+(-.*)?$') {
|
||||
Stop-WithError "Invalid version format: $VersionString`nExpected format: v1.0.0 or 1.0.0"
|
||||
}
|
||||
|
||||
# Ensure version starts with 'v' for GitHub releases
|
||||
if ($VersionString -notmatch '^v') {
|
||||
$VersionString = "v$VersionString"
|
||||
}
|
||||
|
||||
return $VersionString
|
||||
}
|
||||
|
||||
# Check if coolify is already installed
|
||||
function Test-ExistingInstallation {
|
||||
param([string]$NewVersion)
|
||||
|
||||
try {
|
||||
$coolifyCmd = Get-Command coolify -ErrorAction SilentlyContinue
|
||||
if ($coolifyCmd) {
|
||||
$currentVersion = & coolify version 2>$null | Select-Object -First 1
|
||||
if (-not $currentVersion) {
|
||||
$currentVersion = "unknown"
|
||||
}
|
||||
|
||||
Write-Warning "Coolify CLI is already installed: $currentVersion"
|
||||
Write-Host "This will upgrade/reinstall to version " -NoNewline
|
||||
Write-Success $NewVersion
|
||||
|
||||
$response = Read-Host "Continue? [y/N]"
|
||||
if ($response -notmatch '^[Yy]$') {
|
||||
Write-Host "Installation cancelled."
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Coolify not found, continue with installation
|
||||
}
|
||||
}
|
||||
|
||||
# Download and install binary from GitHub release
|
||||
function Install-FromGitHub {
|
||||
param(
|
||||
[string]$Repo,
|
||||
[string]$Release,
|
||||
[string]$Name,
|
||||
[string]$InstallDirectory,
|
||||
[hashtable]$Platform
|
||||
)
|
||||
|
||||
# Clean version (remove 'v' prefix if present)
|
||||
$cleanVersion = $Release -replace '^v', ''
|
||||
|
||||
$filename = "${Name}_${cleanVersion}_$($Platform.OS)_$($Platform.Arch).zip"
|
||||
$downloadUrl = "https://github.com/${Repo}/releases/download/${Release}/${filename}"
|
||||
|
||||
Write-Success "Downloading $Name $Release"
|
||||
Write-Host "Platform: $($Platform.OS)/$($Platform.Arch)"
|
||||
Write-Host "URL: $downloadUrl"
|
||||
|
||||
# Create temp file
|
||||
$Script:TEMP_FILE = [System.IO.Path]::GetTempFileName() + ".zip"
|
||||
|
||||
# Download file
|
||||
try {
|
||||
$ProgressPreference = 'SilentlyContinue' # Speed up download
|
||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $Script:TEMP_FILE -ErrorAction Stop
|
||||
$ProgressPreference = 'Continue'
|
||||
}
|
||||
catch {
|
||||
Stop-WithError "Failed to download from ${downloadUrl}`nPlease check if the version exists or try again later.`nError: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Verify downloaded file is not empty
|
||||
if ((Get-Item $Script:TEMP_FILE).Length -eq 0) {
|
||||
Stop-WithError "Downloaded file is empty"
|
||||
}
|
||||
|
||||
# Create install directory if it doesn't exist
|
||||
if (-not (Test-Path $InstallDirectory)) {
|
||||
Write-Host "Creating directory: $InstallDirectory"
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $InstallDirectory -Force | Out-Null
|
||||
}
|
||||
catch {
|
||||
Stop-WithError "Failed to create directory $InstallDirectory : $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Extract binary
|
||||
Write-Host "Installing $Name to $InstallDirectory\$Script:BINARY_NAME"
|
||||
|
||||
try {
|
||||
# Remove existing binary if present
|
||||
$binaryPath = Join-Path $InstallDirectory $Script:BINARY_NAME
|
||||
if (Test-Path $binaryPath) {
|
||||
Remove-Item -Path $binaryPath -Force
|
||||
}
|
||||
|
||||
# Extract zip file
|
||||
Expand-Archive -Path $Script:TEMP_FILE -DestinationPath $InstallDirectory -Force
|
||||
}
|
||||
catch {
|
||||
Stop-WithError "Failed to extract binary: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Verify installation
|
||||
$installedBinary = Join-Path $InstallDirectory $Script:BINARY_NAME
|
||||
if (-not (Test-Path $installedBinary)) {
|
||||
Stop-WithError "Binary was not installed to $installedBinary"
|
||||
}
|
||||
|
||||
Write-Success "✓ $Name installed successfully to $installedBinary"
|
||||
|
||||
# Add to PATH if not already present
|
||||
Add-ToPath -Directory $InstallDirectory
|
||||
|
||||
# Show installed version
|
||||
try {
|
||||
$installedVersion = & $installedBinary version 2>$null | Select-Object -First 1
|
||||
if ($installedVersion) {
|
||||
Write-Host "Installed version: " -NoNewline
|
||||
Write-Success $installedVersion
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# Version check failed, but installation was successful
|
||||
}
|
||||
}
|
||||
|
||||
# Add directory to PATH
|
||||
function Add-ToPath {
|
||||
param([string]$Directory)
|
||||
|
||||
# Determine which PATH to modify (user or system)
|
||||
$pathScope = if ($User -or -not (Test-Administrator)) { "User" } else { "Machine" }
|
||||
|
||||
# Get current PATH
|
||||
$currentPath = [Environment]::GetEnvironmentVariable("Path", $pathScope)
|
||||
|
||||
# Check if directory is already in PATH
|
||||
$pathDirs = $currentPath -split ';' | ForEach-Object { $_.Trim() }
|
||||
if ($pathDirs -contains $Directory) {
|
||||
Write-Host "Directory is already in PATH"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Adding $Directory to PATH ($pathScope)..."
|
||||
|
||||
try {
|
||||
# Add to PATH
|
||||
$newPath = "$currentPath;$Directory"
|
||||
[Environment]::SetEnvironmentVariable("Path", $newPath, $pathScope)
|
||||
|
||||
# Update current session PATH
|
||||
$env:Path = "$env:Path;$Directory"
|
||||
|
||||
Write-Success "✓ Added to PATH. You may need to restart your terminal for changes to take effect."
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to add to PATH automatically: $($_.Exception.Message)"
|
||||
Write-Host "Please add the following directory to your PATH manually:"
|
||||
Write-Host " $Directory"
|
||||
}
|
||||
}
|
||||
|
||||
# Main installation flow
|
||||
function Install-CoolifyCLI {
|
||||
Write-Host "Coolify CLI Installer for Windows"
|
||||
Write-Host "=================================="
|
||||
Write-Host ""
|
||||
|
||||
# Show help if requested
|
||||
if ($Help) {
|
||||
Show-Help
|
||||
}
|
||||
|
||||
# Detect platform
|
||||
$platform = Get-PlatformInfo
|
||||
|
||||
# Determine version to install
|
||||
$versionToInstall = if ($Version) {
|
||||
Test-VersionFormat -VersionString $Version
|
||||
} else {
|
||||
Get-LatestVersion
|
||||
}
|
||||
|
||||
Write-Host "Version to install: $versionToInstall"
|
||||
Write-Host ""
|
||||
|
||||
# Check existing installation
|
||||
Test-ExistingInstallation -NewVersion $versionToInstall
|
||||
|
||||
# Determine install directory
|
||||
$installDirectory = if ($InstallDir) {
|
||||
$InstallDir
|
||||
} elseif ($User) {
|
||||
$Script:USER_INSTALL_DIR
|
||||
} else {
|
||||
$Script:GLOBAL_INSTALL_DIR
|
||||
}
|
||||
|
||||
# Check for admin rights if installing globally
|
||||
if (-not $User -and -not $InstallDir -and -not (Test-Administrator)) {
|
||||
Write-Warning "Global installation requires administrator privileges."
|
||||
Write-Host "Please run this script as Administrator, or use -User flag for user installation."
|
||||
Write-Host ""
|
||||
$response = Read-Host "Switch to user installation? [Y/n]"
|
||||
if ($response -match '^[Nn]$') {
|
||||
Stop-WithError "Installation cancelled. Please run as Administrator for global installation."
|
||||
}
|
||||
$installDirectory = $Script:USER_INSTALL_DIR
|
||||
}
|
||||
|
||||
$installMode = if ($User -or $installDirectory -eq $Script:USER_INSTALL_DIR) {
|
||||
"User (no admin rights required)"
|
||||
} else {
|
||||
"Global (administrator)"
|
||||
}
|
||||
|
||||
Write-Host "Install mode: $installMode"
|
||||
Write-Host "Install directory: $installDirectory"
|
||||
Write-Host ""
|
||||
|
||||
# Download and install
|
||||
Install-FromGitHub -Repo $Script:REPO -Release $versionToInstall -Name "coolify-cli" -InstallDirectory $installDirectory -Platform $platform
|
||||
|
||||
Write-Host ""
|
||||
Write-Success "Installation complete!"
|
||||
Write-Host "Run 'coolify --help' to get started"
|
||||
Write-Host ""
|
||||
Write-Host "Note: If 'coolify' command is not found, please restart your terminal."
|
||||
}
|
||||
|
||||
# Run main installation
|
||||
try {
|
||||
Install-CoolifyCLI
|
||||
}
|
||||
catch {
|
||||
Write-ErrorMessage "Unexpected error: $($_.Exception.Message)"
|
||||
Write-ErrorMessage $_.ScriptStackTrace
|
||||
Cleanup
|
||||
}
|
||||
finally {
|
||||
Cleanup
|
||||
}
|
||||
+1
-1
@@ -173,7 +173,7 @@ download_from_github() {
|
||||
local name=$3
|
||||
local install_dir=$4
|
||||
|
||||
local filename="${name}_${release}_${OS}_${ARCH}.tar.gz"
|
||||
local filename="${name}_${release#v}_${OS}_${ARCH}.tar.gz"
|
||||
local download_url="https://github.com/${repo}/releases/download/${release}/${filename}"
|
||||
|
||||
echo -e "${GREEN}Downloading ${name} ${release}${NC}"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"uuid": "env-test-uuid-123",
|
||||
"key": "DATABASE_URL",
|
||||
"value": "postgres://localhost/mydb",
|
||||
"real_value": "postgres://user:pass@localhost/mydb",
|
||||
"is_buildtime": true,
|
||||
"is_preview": false,
|
||||
"is_literal": false,
|
||||
"is_shown_once": false,
|
||||
"is_runtime": true,
|
||||
"is_shared": false
|
||||
}
|
||||
Reference in New Issue
Block a user