diff --git a/README.md b/README.md index 40f3938..8cc16bd 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ Commands can use `server` or `servers` interchangeably. - `--git-branch ` - Git branch - `--git-repository ` - Git repository URL - `--domains ` - Domains (comma-separated) + - `--compose-domain =[,]` - Docker Compose service domains (repeatable; update replaces the existing mapping) - `--build-command ` - Build command - `--start-command ` - Start command - `--install-command ` - Install command @@ -160,6 +161,7 @@ Commands can use `server` or `servers` interchangeably. - `--show-timestamps` - Include timestamps in logs - `coolify app move --environment-uuid ` - Move an application to another environment - `coolify app tag list|add|remove` - Manage application tags +- Git-backed application create variants (`public`, `github`, and `deploy-key`) and `app update` support repeatable `--compose-domain =[,]` flags for Docker Compose routing. On update, the supplied entries replace the existing mapping. - All application create variants support `--tag` and `--tags`; create and update expose the full application settings surface. #### Application Environment Variables diff --git a/cmd/application/appflags/compose_domains.go b/cmd/application/appflags/compose_domains.go new file mode 100644 index 0000000..b9a6c82 --- /dev/null +++ b/cmd/application/appflags/compose_domains.go @@ -0,0 +1,42 @@ +package appflags + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + "github.com/coollabsio/coolify-cli/internal/models" +) + +// BindComposeDomainsFlag registers service-specific domains for Docker Compose applications. +func BindComposeDomainsFlag(cmd *cobra.Command) { + cmd.Flags().StringArray("compose-domain", nil, "Docker Compose service domain in =[,] format (repeatable)") +} + +// ApplyComposeDomainsFlag copies explicitly supplied service domains to an API request. +func ApplyComposeDomainsFlag(cmd *cobra.Command, target *[]models.DockerComposeDomain) (bool, error) { + if !cmd.Flags().Changed("compose-domain") { + return false, nil + } + + values, _ := cmd.Flags().GetStringArray("compose-domain") + domains := make([]models.DockerComposeDomain, 0, len(values)) + for _, value := range values { + name, domain, found := strings.Cut(value, "=") + if !found { + return false, fmt.Errorf("invalid --compose-domain %q: expected =", value) + } + name = strings.TrimSpace(name) + if name == "" { + return false, fmt.Errorf("invalid --compose-domain %q: service name cannot be empty", value) + } + domains = append(domains, models.DockerComposeDomain{ + Name: name, + Domain: strings.TrimSpace(domain), + }) + } + + *target = domains + return true, nil +} diff --git a/cmd/application/appflags/compose_domains_test.go b/cmd/application/appflags/compose_domains_test.go new file mode 100644 index 0000000..c9b97d0 --- /dev/null +++ b/cmd/application/appflags/compose_domains_test.go @@ -0,0 +1,79 @@ +package appflags + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/coollabsio/coolify-cli/internal/models" +) + +func TestApplyComposeDomainsFlag_PreservesCommaSeparatedURLsAndEquals(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + BindComposeDomainsFlag(cmd) + require.NoError(t, cmd.Flags().Set("compose-domain", "litellm=https://litellm.example.com")) + require.NoError(t, cmd.Flags().Set("compose-domain", "admin=https://admin.example.com,https://admin2.example.com/login?next=/users&role=admin")) + + var domains []models.DockerComposeDomain + changed, err := ApplyComposeDomainsFlag(cmd, &domains) + + require.NoError(t, err) + assert.True(t, changed) + assert.Equal(t, []models.DockerComposeDomain{ + {Name: "litellm", Domain: "https://litellm.example.com"}, + {Name: "admin", Domain: "https://admin.example.com,https://admin2.example.com/login?next=/users&role=admin"}, + }, domains) +} + +func TestApplyComposeDomainsFlag_AllowsEmptyDomain(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + BindComposeDomainsFlag(cmd) + require.NoError(t, cmd.Flags().Set("compose-domain", "admin=")) + + var domains []models.DockerComposeDomain + changed, err := ApplyComposeDomainsFlag(cmd, &domains) + + require.NoError(t, err) + assert.True(t, changed) + assert.Equal(t, []models.DockerComposeDomain{{Name: "admin", Domain: ""}}, domains) +} + +func TestApplyComposeDomainsFlag_RejectsMalformedValues(t *testing.T) { + tests := []struct { + name string + value string + want string + }{ + {name: "missing separator", value: "litellm", want: "expected ="}, + {name: "empty service", value: " =https://example.com", want: "service name cannot be empty"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + BindComposeDomainsFlag(cmd) + require.NoError(t, cmd.Flags().Set("compose-domain", tt.value)) + + var domains []models.DockerComposeDomain + changed, err := ApplyComposeDomainsFlag(cmd, &domains) + + assert.False(t, changed) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.want) + }) + } +} + +func TestApplyComposeDomainsFlag_OmitsUnchangedFlag(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + BindComposeDomainsFlag(cmd) + + var domains []models.DockerComposeDomain + changed, err := ApplyComposeDomainsFlag(cmd, &domains) + + require.NoError(t, err) + assert.False(t, changed) + assert.Nil(t, domains) +} diff --git a/cmd/application/application_test.go b/cmd/application/application_test.go index f524907..22c3df1 100644 --- a/cmd/application/application_test.go +++ b/cmd/application/application_test.go @@ -29,6 +29,7 @@ func TestNewAppCommand_RegistersMoveAndTagCommands(t *testing.T) { func TestNewApplicationCommands_ExposeParityFlags(t *testing.T) { assert.NotNil(t, NewLogsCommand().Flags().Lookup("show-timestamps")) assert.NotNil(t, NewMoveCommand().Flags().Lookup("environment-uuid")) + assert.NotNil(t, NewUpdateCommand().Flags().Lookup("compose-domain")) for _, flag := range []string{ "disable-build-cache", "docker-images-to-keep", "include-source-commit-in-build", @@ -41,3 +42,19 @@ func TestNewApplicationCommands_ExposeParityFlags(t *testing.T) { assert.NotNil(t, NewUpdateCommand().Flags().Lookup(flag), "missing --%s", flag) } } + +func TestUpdateCommand_RejectsDomainsWithComposeDomain(t *testing.T) { + cmd := NewUpdateCommand() + cmd.SilenceErrors = true + cmd.SilenceUsage = true + cmd.SetArgs([]string{ + "app-uuid", + "--domains", "https://app.example.com", + "--compose-domain", "app=https://app.example.com", + }) + + err := cmd.Execute() + + require.Error(t, err) + assert.Contains(t, err.Error(), "group [domains compose-domain]") +} diff --git a/cmd/application/create/create_test.go b/cmd/application/create/create_test.go index 06f7902..bfb7bb1 100644 --- a/cmd/application/create/create_test.go +++ b/cmd/application/create/create_test.go @@ -25,3 +25,16 @@ func TestCreateCommands_ExposeNewSettingsAndTagsFlags(t *testing.T) { } } } + +func TestGitCreateCommands_ExposeComposeDomainFlag(t *testing.T) { + commands := []*cobra.Command{ + NewPublicCommand(), NewGitHubCommand(), NewDeployKeyCommand(), + } + + for _, command := range commands { + assert.NotNil(t, command.Flags().Lookup("compose-domain"), "%s missing --compose-domain", command.Name()) + } + + assert.Nil(t, NewDockerfileCommand().Flags().Lookup("compose-domain")) + assert.Nil(t, NewDockerImageCommand().Flags().Lookup("compose-domain")) +} diff --git a/cmd/application/create/deploy_key.go b/cmd/application/create/deploy_key.go index df0b280..0301904 100644 --- a/cmd/application/create/deploy_key.go +++ b/cmd/application/create/deploy_key.go @@ -26,9 +26,13 @@ Examples: --private-key-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 --project-uuid --environment-name production \ - --private-key-uuid --git-repository "git@gitlab.com:owner/repo.git" --git-branch main \ - --build-pack dockerfile --ports-exposes 8080 --instant-deploy`, + coolify app create deploy-key --server-uuid --project-uuid --environment-name production \ + --private-key-uuid --git-repository "git@gitlab.com:owner/repo.git" --git-branch main \ + --build-pack dockerfile --ports-exposes 8080 --instant-deploy + + coolify app create deploy-key --server-uuid --project-uuid --environment-name production \ + --private-key-uuid --git-repository "git@gitlab.com:owner/repo.git" --git-branch main \ + --build-pack dockercompose --ports-exposes 4000 --compose-domain "api=https://api.example.com"`, RunE: func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() @@ -81,6 +85,9 @@ Examples: setOptionalStringFlag(cmd, "name", &req.Name) setOptionalStringFlag(cmd, "description", &req.Description) setOptionalStringFlag(cmd, "domains", &req.Domains) + if _, err := appflags.ApplyComposeDomainsFlag(cmd, &req.DockerComposeDomains); err != nil { + return err + } setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA) setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID) setOptionalStringFlag(cmd, "build-command", &req.BuildCommand) @@ -138,6 +145,7 @@ Examples: cmd.Flags().String("name", "", "Application name") cmd.Flags().String("description", "", "Application description") cmd.Flags().String("domains", "", "Domain(s) for the application") + appflags.BindComposeDomainsFlag(cmd) 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") @@ -154,6 +162,7 @@ Examples: cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage") appflags.BindSettingsFlags(cmd) appflags.BindTagsFlag(cmd) + cmd.MarkFlagsMutuallyExclusive("domains", "compose-domain") return cmd } diff --git a/cmd/application/create/github.go b/cmd/application/create/github.go index da2501a..569a0eb 100644 --- a/cmd/application/create/github.go +++ b/cmd/application/create/github.go @@ -27,9 +27,13 @@ Examples: --github-app-uuid --git-repository "owner/repo" --git-branch main \ --build-pack nixpacks --ports-exposes 3000 - coolify app create github --server-uuid --project-uuid --environment-name production \ - --github-app-uuid --git-repository "owner/repo" --git-branch main \ - --build-pack dockerfile --ports-exposes 8080 --instant-deploy`, + coolify app create github --server-uuid --project-uuid --environment-name production \ + --github-app-uuid --git-repository "owner/repo" --git-branch main \ + --build-pack dockerfile --ports-exposes 8080 --instant-deploy + + coolify app create github --server-uuid --project-uuid --environment-name production \ + --github-app-uuid --git-repository "owner/repo" --git-branch main \ + --build-pack dockercompose --ports-exposes 4000 --compose-domain "api=https://api.example.com"`, RunE: func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() @@ -82,6 +86,9 @@ Examples: setOptionalStringFlag(cmd, "name", &req.Name) setOptionalStringFlag(cmd, "description", &req.Description) setOptionalStringFlag(cmd, "domains", &req.Domains) + if _, err := appflags.ApplyComposeDomainsFlag(cmd, &req.DockerComposeDomains); err != nil { + return err + } setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA) setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID) setOptionalStringFlag(cmd, "build-command", &req.BuildCommand) @@ -139,6 +146,7 @@ Examples: cmd.Flags().String("name", "", "Application name") cmd.Flags().String("description", "", "Application description") cmd.Flags().String("domains", "", "Domain(s) for the application") + appflags.BindComposeDomainsFlag(cmd) 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") @@ -155,6 +163,7 @@ Examples: cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage") appflags.BindSettingsFlags(cmd) appflags.BindTagsFlag(cmd) + cmd.MarkFlagsMutuallyExclusive("domains", "compose-domain") return cmd } diff --git a/cmd/application/create/public.go b/cmd/application/create/public.go index 691937c..cc6a7b4 100644 --- a/cmd/application/create/public.go +++ b/cmd/application/create/public.go @@ -23,9 +23,13 @@ Examples: coolify app create public --server-uuid --project-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 --project-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"`, + coolify app create public --server-uuid --project-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" + + coolify app create public --server-uuid --project-uuid --environment-name production \ + --git-repository "https://github.com/user/repo" --git-branch main --build-pack dockercompose --ports-exposes 4000 \ + --compose-domain "api=https://api.example.com" --compose-domain "admin=https://admin.example.com,https://admin2.example.com"`, RunE: func(cmd *cobra.Command, _ []string) error { ctx := cmd.Context() @@ -73,6 +77,9 @@ Examples: setOptionalStringFlag(cmd, "name", &req.Name) setOptionalStringFlag(cmd, "description", &req.Description) setOptionalStringFlag(cmd, "domains", &req.Domains) + if _, err := appflags.ApplyComposeDomainsFlag(cmd, &req.DockerComposeDomains); err != nil { + return err + } setOptionalStringFlag(cmd, "git-commit-sha", &req.GitCommitSHA) setOptionalStringFlag(cmd, "destination-uuid", &req.DestinationUUID) setOptionalStringFlag(cmd, "build-command", &req.BuildCommand) @@ -129,6 +136,7 @@ Examples: cmd.Flags().String("name", "", "Application name") cmd.Flags().String("description", "", "Application description") cmd.Flags().String("domains", "", "Domain(s) for the application") + appflags.BindComposeDomainsFlag(cmd) 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") @@ -145,6 +153,7 @@ Examples: cmd.Flags().String("dockerfile-target-build", "", "Dockerfile target build stage") appflags.BindSettingsFlags(cmd) appflags.BindTagsFlag(cmd) + cmd.MarkFlagsMutuallyExclusive("domains", "compose-domain") return cmd } diff --git a/cmd/application/update.go b/cmd/application/update.go index 195fa38..a432806 100644 --- a/cmd/application/update.go +++ b/cmd/application/update.go @@ -16,8 +16,14 @@ func NewUpdateCommand() *cobra.Command { cmd := &cobra.Command{ Use: "update ", Short: "Update application configuration", - Long: `Update configuration for a specific application. Only specified fields will be updated.`, - Args: cli.ExactArgs(1, ""), + Long: `Update configuration for a specific application. Only specified fields will be updated. + +Docker Compose domains are supplied as the complete service-to-domain mapping for the update. + +Example: + coolify app update --compose-domain "api=https://api.example.com" \ + --compose-domain "admin=https://admin.example.com,https://admin2.example.com"`, + Args: cli.ExactArgs(1, ""), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() uuid := args[0] @@ -55,6 +61,11 @@ func NewUpdateCommand() *cobra.Command { req.Domains = &domains hasUpdates = true } + if changed, err := appflags.ApplyComposeDomainsFlag(cmd, &req.DockerComposeDomains); err != nil { + return err + } else if changed { + hasUpdates = true + } if cmd.Flags().Changed("build-command") { buildCmd, _ := cmd.Flags().GetString("build-command") req.BuildCommand = &buildCmd @@ -153,6 +164,7 @@ func NewUpdateCommand() *cobra.Command { cmd.Flags().String("git-branch", "", "Git branch") cmd.Flags().String("git-repository", "", "Git repository URL") cmd.Flags().String("domains", "", "Domains (comma-separated)") + appflags.BindComposeDomainsFlag(cmd) cmd.Flags().String("build-command", "", "Build command") cmd.Flags().String("start-command", "", "Start command") cmd.Flags().String("install-command", "", "Install command") @@ -167,6 +179,7 @@ func NewUpdateCommand() *cobra.Command { cmd.Flags().Bool("health-check-enabled", false, "Enable health check") cmd.Flags().String("health-check-path", "", "Health check path") appflags.BindSettingsFlags(cmd) + cmd.MarkFlagsMutuallyExclusive("domains", "compose-domain") return cmd } diff --git a/cmd/docs.go b/cmd/docs.go index 0f80455..b5dc92c 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -199,6 +199,7 @@ coolify app move --environment-uuid coolify app tag add production coolify app deployments list coolify app deployments logs --follow +coolify app update --compose-domain api=https://api.example.com --compose-domain admin=https://admin.example.com,https://admin2.example.com ` + "```" + ` ### Environment Variables diff --git a/internal/models/application.go b/internal/models/application.go index f2253aa..92ceb03 100644 --- a/internal/models/application.go +++ b/internal/models/application.go @@ -173,20 +173,21 @@ type ApplicationListItem struct { type ApplicationUpdateRequest struct { ApplicationSettingsRequest - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - GitBranch *string `json:"git_branch,omitempty"` - GitRepository *string `json:"git_repository,omitempty"` - GitCommitSHA *string `json:"git_commit_sha,omitempty"` - Domains *string `json:"domains,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"` - BuildPack *string `json:"build_pack,omitempty"` - PortsExposes *string `json:"ports_exposes,omitempty"` - PortsMappings *string `json:"ports_mappings,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + GitBranch *string `json:"git_branch,omitempty"` + GitRepository *string `json:"git_repository,omitempty"` + GitCommitSHA *string `json:"git_commit_sha,omitempty"` + Domains *string `json:"domains,omitempty"` + DockerComposeDomains []DockerComposeDomain `json:"docker_compose_domains,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"` + BuildPack *string `json:"build_pack,omitempty"` + PortsExposes *string `json:"ports_exposes,omitempty"` + PortsMappings *string `json:"ports_mappings,omitempty"` // Docker configuration Dockerfile *string `json:"dockerfile,omitempty"` @@ -231,6 +232,12 @@ type ApplicationUpdateRequest struct { IsStatic *bool `json:"is_static,omitempty"` } +// DockerComposeDomain assigns one or more URLs to a Docker Compose service. +type DockerComposeDomain struct { + Name string `json:"name"` + Domain string `json:"domain"` +} + // ApplicationLifecycleResponse represents the response from lifecycle operations type ApplicationLifecycleResponse struct { Message string `json:"message"` @@ -440,21 +447,22 @@ type ApplicationCreatePublicRequest struct { 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"` - DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Domains *string `json:"domains,omitempty"` + DockerComposeDomains []DockerComposeDomain `json:"docker_compose_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"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` // Health checks HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` @@ -487,28 +495,29 @@ type ApplicationCreateGitHubAppRequest struct { 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"` - DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` - HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` - HealthCheckPath *string `json:"health_check_path,omitempty"` - HealthCheckPort *string `json:"health_check_port,omitempty"` - HealthCheckMethod *string `json:"health_check_method,omitempty"` - LimitsCPUs *string `json:"limits_cpus,omitempty"` - LimitsMemory *string `json:"limits_memory,omitempty"` - Tags []string `json:"tags,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Domains *string `json:"domains,omitempty"` + DockerComposeDomains []DockerComposeDomain `json:"docker_compose_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"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` + HealthCheckPath *string `json:"health_check_path,omitempty"` + HealthCheckPort *string `json:"health_check_port,omitempty"` + HealthCheckMethod *string `json:"health_check_method,omitempty"` + LimitsCPUs *string `json:"limits_cpus,omitempty"` + LimitsMemory *string `json:"limits_memory,omitempty"` + Tags []string `json:"tags,omitempty"` } // ApplicationCreateDeployKeyRequest for POST /applications/private-deploy-key @@ -530,28 +539,29 @@ type ApplicationCreateDeployKeyRequest struct { 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"` - DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` - HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` - HealthCheckPath *string `json:"health_check_path,omitempty"` - HealthCheckPort *string `json:"health_check_port,omitempty"` - HealthCheckMethod *string `json:"health_check_method,omitempty"` - LimitsCPUs *string `json:"limits_cpus,omitempty"` - LimitsMemory *string `json:"limits_memory,omitempty"` - Tags []string `json:"tags,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Domains *string `json:"domains,omitempty"` + DockerComposeDomains []DockerComposeDomain `json:"docker_compose_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"` + DockerfileTargetBuild *string `json:"dockerfile_target_build,omitempty"` + HealthCheckEnabled *bool `json:"health_check_enabled,omitempty"` + HealthCheckPath *string `json:"health_check_path,omitempty"` + HealthCheckPort *string `json:"health_check_port,omitempty"` + HealthCheckMethod *string `json:"health_check_method,omitempty"` + LimitsCPUs *string `json:"limits_cpus,omitempty"` + LimitsMemory *string `json:"limits_memory,omitempty"` + Tags []string `json:"tags,omitempty"` } // ApplicationCreateDockerfileRequest for POST /applications/dockerfile diff --git a/internal/models/application_test.go b/internal/models/application_test.go index 72347fb..50fb848 100644 --- a/internal/models/application_test.go +++ b/internal/models/application_test.go @@ -83,3 +83,42 @@ func TestApplicationCreateSettings_MarshalNewAPIFields(t *testing.T) { assert.Contains(t, body, field) } } + +func TestDockerComposeDomains_MarshalRequestShape(t *testing.T) { + domains := []DockerComposeDomain{ + {Name: "litellm", Domain: "https://litellm.example.com"}, + {Name: "admin", Domain: "https://admin.example.com,https://admin2.example.com"}, + } + tests := []struct { + name string + req any + }{ + {name: "public create", req: ApplicationCreatePublicRequest{DockerComposeDomains: domains}}, + {name: "github create", req: ApplicationCreateGitHubAppRequest{DockerComposeDomains: domains}}, + {name: "deploy key create", req: ApplicationCreateDeployKeyRequest{DockerComposeDomains: domains}}, + {name: "update", req: ApplicationUpdateRequest{DockerComposeDomains: domains}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.req) + require.NoError(t, err) + + var body map[string]any + require.NoError(t, json.Unmarshal(data, &body)) + items, ok := body["docker_compose_domains"].([]any) + require.True(t, ok) + require.Len(t, items, 2) + assert.Equal(t, map[string]any{ + "name": "litellm", + "domain": "https://litellm.example.com", + }, items[0]) + }) + } +} + +func TestDockerComposeDomains_OmittedWhenUnset(t *testing.T) { + data, err := json.Marshal(ApplicationUpdateRequest{}) + require.NoError(t, err) + assert.NotContains(t, string(data), "docker_compose_domains") +} diff --git a/internal/service/application_test.go b/internal/service/application_test.go index 4ca3800..1b719ff 100644 --- a/internal/service/application_test.go +++ b/internal/service/application_test.go @@ -1133,6 +1133,74 @@ func TestApplicationService_CreatePublic(t *testing.T) { assert.Equal(t, "My App", result.Name) } +func TestApplicationService_CreatePublic_SendsDockerComposeDomains(t *testing.T) { + domains := []models.DockerComposeDomain{ + {Name: "litellm", Domain: "https://litellm.example.com"}, + {Name: "admin", Domain: "https://admin.example.com,https://admin2.example.com"}, + } + 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, http.MethodPost, r.Method) + + var body map[string]any + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + items, ok := body["docker_compose_domains"].([]any) + require.True(t, ok) + require.Len(t, items, 2) + assert.Equal(t, "litellm", items[0].(map[string]any)["name"]) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte(`{"uuid":"new-app-uuid","name":"Compose App"}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + req := &models.ApplicationCreatePublicRequest{ + ProjectUUID: "proj-uuid", + ServerUUID: "server-uuid", + GitRepository: "https://github.com/user/repo", + GitBranch: "main", + BuildPack: "dockercompose", + PortsExposes: "4000", + DockerComposeDomains: domains, + } + + _, err := svc.CreatePublic(context.Background(), req) + require.NoError(t, err) +} + +func TestApplicationService_Update_SendsDockerComposeDomains(t *testing.T) { + domains := []models.DockerComposeDomain{ + {Name: "litellm", Domain: "https://litellm.example.com"}, + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/api/v1/applications/app-uuid", r.URL.Path) + assert.Equal(t, http.MethodPatch, r.Method) + + var body map[string]any + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + assert.NotContains(t, body, "domains") + items, ok := body["docker_compose_domains"].([]any) + require.True(t, ok) + require.Len(t, items, 1) + assert.Equal(t, "https://litellm.example.com", items[0].(map[string]any)["domain"]) + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"uuid":"app-uuid","name":"Compose App"}`)) + })) + defer server.Close() + + client := api.NewClient(server.URL, "test-token") + svc := NewApplicationService(client) + + _, err := svc.Update(context.Background(), "app-uuid", models.ApplicationUpdateRequest{ + DockerComposeDomains: domains, + }) + require.NoError(t, err) +} + func TestApplicationService_CreatePublic_Error(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) diff --git a/llms-full.txt b/llms-full.txt index ef0557e..babfd6b 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -208,6 +208,10 @@ Parameters: type: string description: Build pack: nixpacks, static, dockerfile, dockercompose (required) required: true + - name: --compose-domain + type: stringArray + description: Docker Compose service domain in =[,] format (repeatable) + required: false - name: --description type: string description: Application description @@ -719,6 +723,10 @@ Parameters: type: string description: Build pack: nixpacks, static, dockerfile, dockercompose (required) required: true + - name: --compose-domain + type: stringArray + description: Docker Compose service domain in =[,] format (repeatable) + required: false - name: --description type: string description: Application description @@ -912,6 +920,10 @@ Parameters: type: string description: Build pack: nixpacks, static, dockerfile, dockercompose (required) required: true + - name: --compose-domain + type: stringArray + description: Docker Compose service domain in =[,] format (repeatable) + required: false - name: --description type: string description: Application description @@ -1413,7 +1425,7 @@ Description: Remove an application tag Parameters: (None) Command: coolify app update -Description: Update configuration for a specific application. Only specified fields will be updated. +Description: Update application configuration Parameters: - name: --base-directory type: string @@ -1423,6 +1435,10 @@ Parameters: type: string description: Build command required: false + - name: --compose-domain + type: stringArray + description: Docker Compose service domain in =[,] format (repeatable) + required: false - name: --description type: string description: Application description @@ -3066,7 +3082,12 @@ Parameters: (None) Command: coolify server validate Description: Validate a server -Parameters: (None) +Parameters: + - name: --install + type: boolean + description: Install missing prerequisites and Docker (may restart Docker) + required: false + default: false Command: coolify server vultr create Description: Create a Vultr server diff --git a/llms.txt b/llms.txt index e2f8313..5f2132b 100644 --- a/llms.txt +++ b/llms.txt @@ -92,6 +92,7 @@ coolify app move --environment-uuid coolify app tag add production coolify app deployments list coolify app deployments logs --follow +coolify app update --compose-domain api=https://api.example.com --compose-domain admin=https://admin.example.com,https://admin2.example.com ``` ### Environment Variables