Compare commits

..

10 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
24 changed files with 1765 additions and 32 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
}
+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)
}
+1 -1
View File
@@ -14,7 +14,7 @@ import (
// Version variables injected by GoReleaser at build time via ldflags
var (
version = "v1.2"
version = "v1.4.0"
)
// GitHubAPIURL is the URL for fetching CLI version tags (exported for testing)