Compare commits

..

13 Commits

Author SHA1 Message Date
Andras Bacsai bd65345df8 Merge pull request #51 from YaRissi/fix/service-update-env
fix: update service env command
2026-03-19 22:03:20 +01:00
Andras Bacsai c94e147639 feat(env): enforce minimum version requirement for updates 2026-03-19 22:00:40 +01:00
Andras Bacsai 0ea34284ef fix(env): require key and value flags for updating variables
- Changed app and service env update commands to accept only the resource UUID instead of separate env UUID argument
- Made --key and --value required flags for identifying and updating environment variables
- Removed UUID field from EnvironmentVariableUpdateRequest model
- Updated validation to explicitly require --key and --value instead of "at least one field"
- Changed ServiceEnvBulkUpdateResponse from struct with message to slice of ServiceEnvironmentVariable
- Updated BulkUpdateEnvs return type from pointer to non-pointer
- Updated tests and documentation to reflect new command interface
2026-03-19 21:57:11 +01:00
YaRissi a93872ee16 fix lint 2025-12-19 18:49:13 +01:00
YaRissi 1bc1a601a8 fix update env command 2025-12-19 18:42:42 +01:00
github-actions[bot] 4ad94e2d65 chore: bump version to v1.4.0 2025-12-12 13:04:47 +00:00
Andras Bacsai faa8186301 Merge pull request #47 from coollabsio/project-app-create
feat: add create commands for applications, projects, and services
2025-12-12 14:02:09 +01:00
Andras Bacsai 1eba511544 fix: use assert instead of require in HTTP handlers
Replace require.NoError with assert.NoError inside HTTP handler
functions to fix testifylint go-require violations. Using require
in HTTP handlers can cause unpredictable test behavior since
t.FailNow() only exits the current goroutine (the handler), not
the main test goroutine.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 10:21:33 +01:00
Andras Bacsai 541f633edc feat: add create commands for applications, projects, and services
Add comprehensive create functionality for three main resource types:
- Applications: public, private (GitHub App & deploy key), Dockerfile, Docker image
- Projects: simple project creation with optional description
- Services: one-click service deployment with 80+ service types

Includes full service layer implementation with 15+ test cases covering success and error scenarios. Also fixed EnvironmentUUID handling in service creation requests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-06 18:41:31 +01:00
github-actions[bot] 0f23b029f0 chore: bump version to v1.3.0 2025-12-05 12:43:09 +00:00
github-actions[bot] 780b3674c7 chore: bump version to v1.2 2025-12-05 12:38:45 +00:00
Andras Bacsai 77adbfaebc Merge pull request #46 from coollabsio/update-check-every-cmd
Check for CLI updates on every command
2025-12-05 13:35:52 +01:00
Andras Bacsai 9215fd537e feat: check for CLI updates on every command
Remove the 10-minute check interval so update notifications appear on every command execution. Silent error handling prevents network issues from interrupting commands. Updated message format is more concise and shows the available version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 13:35:05 +01:00
26 changed files with 2069 additions and 95 deletions
+1
View File
@@ -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`)
+15 -2
View File
@@ -149,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)
@@ -232,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)
+2
View File
@@ -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())
+38
View File
@@ -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
}
+152
View File
@@ -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
}
+120
View File
@@ -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
}
+127
View File
@@ -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
}
+153
View File
@@ -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
}
+158
View File
@@ -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
}
}
+13 -8
View File
@@ -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
@@ -59,8 +61,11 @@ func NewUpdateEnvCommand() *cobra.Command {
req.IsRuntime = &isRuntime
}
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsPreview == nil && req.IsLiteral == nil && req.IsMultiline == nil && req.IsRuntime == 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)
+70
View File
@@ -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
}
+1
View File
@@ -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
View File
@@ -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)
}
+252
View File
@@ -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
}
+13 -9
View File
@@ -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.ServiceEnvironmentVariableUpdateRequest{
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")
@@ -56,9 +58,11 @@ func NewUpdateCommand() *cobra.Command {
req.IsRuntime = &isRuntime
}
// Check if at least one field is being updated
if req.Key == nil && req.Value == nil && req.IsBuildTime == nil && req.IsLiteral == nil && req.IsMultiline == nil && req.IsRuntime == nil {
return fmt.Errorf("at least one field must be provided to update (--key, --value, --build-time, --is-literal, --is-multiline, or --runtime)")
if req.Key == nil {
return fmt.Errorf("--key is required")
}
if req.Value == nil {
return fmt.Errorf("--value is required")
}
serviceSvc := service.NewService(client)
+1
View File
@@ -18,6 +18,7 @@ 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())
+181 -1
View File
@@ -125,7 +125,6 @@ type EnvironmentVariableCreateRequest struct {
// 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"`
@@ -134,3 +133,184 @@ type EnvironmentVariableUpdateRequest struct {
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"`
}
+3 -5
View File
@@ -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"`
@@ -99,7 +100,6 @@ type ServiceEnvironmentVariableCreateRequest struct {
// ServiceEnvironmentVariableUpdateRequest represents the request to update a service environment variable
type ServiceEnvironmentVariableUpdateRequest struct {
UUID string `json:"uuid"`
Key *string `json:"key,omitempty"`
Value *string `json:"value,omitempty"`
IsBuildTime *bool `json:"is_build_time,omitempty"`
@@ -114,6 +114,4 @@ type ServiceEnvBulkUpdateRequest struct {
}
// ServiceEnvBulkUpdateResponse represents the response from service bulk update
type ServiceEnvBulkUpdateResponse struct {
Message string `json:"message,omitempty"`
}
type ServiceEnvBulkUpdateResponse []ServiceEnvironmentVariable
+50
View File
@@ -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
}
+251 -2
View File
@@ -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,
}
@@ -947,3 +949,250 @@ func TestApplicationService_EnvRuntimeAndShared(t *testing.T) {
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)
}
+10
View File
@@ -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
}
+68
View File
@@ -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)
}
+2 -2
View File
@@ -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 *models.ServiceEnvBulkUpdateRequest) (*models.ServiceEnvBulkUpdateResponse, error) {
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
}
+83 -2
View File
@@ -330,8 +330,7 @@ func TestService_UpdateEnv(t *testing.T) {
newKey := "UPDATED_VAR"
env, err := svc.UpdateEnv(context.Background(), "service-uuid-123", &models.ServiceEnvironmentVariableUpdateRequest{
UUID: "env-123",
Key: &newKey,
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
View File
@@ -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.1"
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
}
+262
View File
@@ -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)
}
}