diff --git a/examples/resources/coolify_application/deploy_key.tf b/examples/resources/coolify_application/deploy_key.tf new file mode 100644 index 0000000..6f0cf82 --- /dev/null +++ b/examples/resources/coolify_application/deploy_key.tf @@ -0,0 +1,19 @@ +# Example: Private Deploy Key Application + +resource "coolify_application" "deploy_key_example" { + source_type = "private-deploy-key" + + project_uuid = "your-project-uuid" + server_uuid = "your-server-uuid" + environment_name = "production" + + private_key_uuid = "your-private-key-uuid" + git_repository = "git@github.com:your-org/your-private-repo.git" + git_branch = "main" + build_pack = "nixpacks" + ports_exposes = "80" + + name = "Deploy Key Example" + description = "Application from private repository via deploy key" +} + diff --git a/examples/resources/coolify_application/dockercompose.tf b/examples/resources/coolify_application/dockercompose.tf new file mode 100644 index 0000000..08226fe --- /dev/null +++ b/examples/resources/coolify_application/dockercompose.tf @@ -0,0 +1,25 @@ +# Example: Docker Compose Application + +resource "coolify_application" "dockercompose_example" { + source_type = "dockercompose" + + project_uuid = "your-project-uuid" + server_uuid = "your-server-uuid" + environment_name = "production" + + docker_compose_raw = < +# Example: ./import.sh rg8ks8c + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +terraform import coolify_application.example "$1" + diff --git a/examples/resources/coolify_application/public.tf b/examples/resources/coolify_application/public.tf new file mode 100644 index 0000000..233f1b7 --- /dev/null +++ b/examples/resources/coolify_application/public.tf @@ -0,0 +1,18 @@ +# Example: Public Git Repository Application + +resource "coolify_application" "public_example" { + source_type = "public" + + project_uuid = "your-project-uuid" + server_uuid = "your-server-uuid" + environment_name = "production" + + git_repository = "https://github.com/coollabsio/coolify" + git_branch = "main" + build_pack = "nixpacks" + ports_exposes = "80" + + name = "Public App Example" + description = "Application from public Git repository" +} + diff --git a/internal/expand/string.go b/internal/expand/string.go index 7b89d28..2158e9a 100644 --- a/internal/expand/string.go +++ b/internal/expand/string.go @@ -10,6 +10,21 @@ func String(value types.String) *string { return value.ValueStringPointer() } +// StringOrNil returns nil if the string is null, unknown, or empty. +// This is useful for fields that should not be sent to the API if empty. +func StringOrNil(value types.String) *string { + if value.IsNull() || value.IsUnknown() { + return nil + } + + str := value.ValueString() + if str == "" { + return nil + } + + return &str +} + func RequiredString(value types.String) string { if value.IsNull() || value.IsUnknown() { return "" diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 1c66320..3ff052a 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -215,6 +215,7 @@ func (p *CoolifyProvider) Resources(ctx context.Context) []func() resource.Resou private_key.NewPrivateKeyResource, service.NewServerResource, service.NewProjectResource, + service.NewApplicationResource, service.NewApplicationEnvsResource, service.NewServiceEnvsResource, service.NewPostgresqlDatabaseResource, diff --git a/internal/service/application_envs_resource.go b/internal/service/application_envs_resource.go index 58201f8..33c6849 100644 --- a/internal/service/application_envs_resource.go +++ b/internal/service/application_envs_resource.go @@ -102,11 +102,10 @@ func (r *applicationEnvsResource) Create(ctx context.Context, req resource.Creat uuid := plan.Uuid.ValueString() for i, env := range plan.Env { createResp, err := r.client.CreateEnvByApplicationUuidWithResponse(ctx, uuid, api.CreateEnvByApplicationUuidJSONRequestBody{ - IsBuildTime: env.IsBuildTime.ValueBoolPointer(), - IsLiteral: env.IsLiteral.ValueBoolPointer(), - IsPreview: env.IsPreview.ValueBoolPointer(), - Key: env.Key.ValueStringPointer(), - Value: env.Value.ValueStringPointer(), + IsLiteral: env.IsLiteral.ValueBoolPointer(), + IsPreview: env.IsPreview.ValueBoolPointer(), + Key: env.Key.ValueStringPointer(), + Value: env.Value.ValueStringPointer(), }) if err != nil { @@ -125,10 +124,43 @@ func (r *applicationEnvsResource) Create(ctx context.Context, req resource.Creat return } - // Set the UUID from the response plan.Env[i].Uuid = types.StringPointerValue(createResp.JSON201.Uuid) } + var bulkUpdateEnvs = []updateEnvsByApplicationUuidJSONRequestBodyItem{} + for _, env := range plan.Env { + bulkUpdateEnvs = append(bulkUpdateEnvs, updateEnvsByApplicationUuidJSONRequestBodyItem{ + IsBuildTime: env.IsBuildTime.ValueBoolPointer(), + IsLiteral: env.IsLiteral.ValueBoolPointer(), + IsMultiline: env.IsMultiline.ValueBoolPointer(), + IsPreview: env.IsPreview.ValueBoolPointer(), + IsShownOnce: env.IsShownOnce.ValueBoolPointer(), + Key: env.Key.ValueStringPointer(), + Value: env.Value.ValueStringPointer(), + }) + } + + if len(bulkUpdateEnvs) > 0 { + updateResp, err := r.client.UpdateEnvsByApplicationUuidWithResponse(ctx, uuid, api.UpdateEnvsByApplicationUuidJSONRequestBody{ + Data: bulkUpdateEnvs, + }) + + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error updating application envs after creation: uuid=%s", uuid), + err.Error(), + ) + return + } + + if updateResp.StatusCode() != http.StatusCreated { + resp.Diagnostics.AddError( + "Unexpected HTTP status code updating application envs after creation", + fmt.Sprintf("Received %s updating application envs: uuid=%s. Details: %s", updateResp.Status(), uuid, updateResp.Body)) + return + } + } + data, _ := r.readFromAPI(ctx, &resp.Diagnostics, uuid) data.Env = r.filterRelevantEnvs(plan.Env, data.Env) @@ -178,26 +210,22 @@ func (r *applicationEnvsResource) Update(ctx context.Context, req resource.Updat uuid := plan.Uuid.ValueString() - // Update API call logic tflog.Debug(ctx, "Updating application envs", map[string]interface{}{ "uuid": uuid, }) - // Create a map of current state envs for fast lookup stateEnvs := make(map[string]resource_application_envs.ApplicationEnvsModel) for _, env := range state.Env { key := fmt.Sprintf("%s-%t", env.Key.ValueString(), env.IsPreview.ValueBool()) stateEnvs[key] = env } - // Create a map of plan envs for fast lookup planEnvs := make(map[string]resource_application_envs.ApplicationEnvsModel) for _, env := range plan.Env { key := fmt.Sprintf("%s-%t", env.Key.ValueString(), env.IsPreview.ValueBool()) planEnvs[key] = env } - // Delete envs that are in state but not in plan for key, env := range stateEnvs { if _, exists := planEnvs[key]; !exists { _, err := r.client.DeleteEnvByApplicationUuidWithResponse(ctx, uuid, env.Uuid.ValueString()) @@ -257,7 +285,6 @@ func (r *applicationEnvsResource) Update(ctx context.Context, req resource.Updat func (r *applicationEnvsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state applicationEnvsResourceModel - // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return @@ -276,8 +303,6 @@ func (r *applicationEnvsResource) ImportState(ctx context.Context, req resource. resource.ImportStatePassthroughID(ctx, path.Root("uuid"), req, resp) } -// MARK: Helper Functions - func (r *applicationEnvsResource) filterRelevantEnvs( stateEnvs []resource_application_envs.ApplicationEnvsModel, apiEnvs []resource_application_envs.ApplicationEnvsModel, diff --git a/internal/service/application_model.go b/internal/service/application_model.go new file mode 100644 index 0000000..e55e605 --- /dev/null +++ b/internal/service/application_model.go @@ -0,0 +1,967 @@ +package service + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + + "terraform-provider-coolify/internal/api" + "terraform-provider-coolify/internal/expand" + "terraform-provider-coolify/internal/flatten" +) + +type ApplicationSourceType string + +const ( + ApplicationSourceTypePublic ApplicationSourceType = "public" + ApplicationSourceTypePrivateGithubApp ApplicationSourceType = "private-github-app" + ApplicationSourceTypePrivateDeployKey ApplicationSourceType = "private-deploy-key" + ApplicationSourceTypeDockerfile ApplicationSourceType = "dockerfile" + ApplicationSourceTypeDockerimage ApplicationSourceType = "dockerimage" + ApplicationSourceTypeDockercompose ApplicationSourceType = "dockercompose" +) + +type ApplicationModel struct { + Uuid types.String `tfsdk:"uuid"` + + // Source type discriminant + SourceType types.String `tfsdk:"source_type"` + + // Common fields + ProjectUuid types.String `tfsdk:"project_uuid"` + ServerUuid types.String `tfsdk:"server_uuid"` + EnvironmentName types.String `tfsdk:"environment_name"` + EnvironmentUuid types.String `tfsdk:"environment_uuid"` + DestinationUuid types.String `tfsdk:"destination_uuid"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Domains types.String `tfsdk:"domains"` + InstantDeploy types.Bool `tfsdk:"instant_deploy"` + + // Git-based source fields (public, private-github-app, private-deploy-key) + GitRepository types.String `tfsdk:"git_repository"` + GitBranch types.String `tfsdk:"git_branch"` + BuildPack types.String `tfsdk:"build_pack"` + PortsExposes types.String `tfsdk:"ports_exposes"` + + // Private GitHub App specific + GithubAppUuid types.String `tfsdk:"github_app_uuid"` + + // Private Deploy Key specific + PrivateKeyUuid types.String `tfsdk:"private_key_uuid"` + + // Dockerfile specific + Dockerfile types.String `tfsdk:"dockerfile"` + + // Docker Image specific + DockerRegistryImageName types.String `tfsdk:"docker_registry_image_name"` + DockerRegistryImageTag types.String `tfsdk:"docker_registry_image_tag"` + + // Docker Compose specific + DockerComposeRaw types.String `tfsdk:"docker_compose_raw"` + + // Optional common fields + BaseDirectory types.String `tfsdk:"base_directory"` + BuildCommand types.String `tfsdk:"build_command"` + StartCommand types.String `tfsdk:"start_command"` + InstallCommand types.String `tfsdk:"install_command"` + PublishDirectory types.String `tfsdk:"publish_directory"` + PortsMappings types.String `tfsdk:"ports_mappings"` + GitCommitSha types.String `tfsdk:"git_commit_sha"` + IsStatic types.Bool `tfsdk:"is_static"` + StaticImage types.String `tfsdk:"static_image"` + HealthCheckEnabled types.Bool `tfsdk:"health_check_enabled"` + HealthCheckPath types.String `tfsdk:"health_check_path"` + HealthCheckPort types.String `tfsdk:"health_check_port"` + HealthCheckHost types.String `tfsdk:"health_check_host"` + HealthCheckMethod types.String `tfsdk:"health_check_method"` + HealthCheckReturnCode types.Int64 `tfsdk:"health_check_return_code"` + HealthCheckScheme types.String `tfsdk:"health_check_scheme"` + HealthCheckResponseText types.String `tfsdk:"health_check_response_text"` + HealthCheckInterval types.Int64 `tfsdk:"health_check_interval"` + HealthCheckTimeout types.Int64 `tfsdk:"health_check_timeout"` + HealthCheckRetries types.Int64 `tfsdk:"health_check_retries"` + HealthCheckStartPeriod types.Int64 `tfsdk:"health_check_start_period"` + LimitsMemory types.String `tfsdk:"limits_memory"` + LimitsMemorySwap types.String `tfsdk:"limits_memory_swap"` + LimitsMemorySwappiness types.Int64 `tfsdk:"limits_memory_swappiness"` + LimitsMemoryReservation types.String `tfsdk:"limits_memory_reservation"` + LimitsCpus types.String `tfsdk:"limits_cpus"` + LimitsCpuset types.String `tfsdk:"limits_cpuset"` + LimitsCpuShares types.Int64 `tfsdk:"limits_cpu_shares"` + CustomLabels types.String `tfsdk:"custom_labels"` + CustomDockerRunOptions types.String `tfsdk:"custom_docker_run_options"` + PostDeploymentCommand types.String `tfsdk:"post_deployment_command"` + PostDeploymentCommandContainer types.String `tfsdk:"post_deployment_command_container"` + PreDeploymentCommand types.String `tfsdk:"pre_deployment_command"` + PreDeploymentCommandContainer types.String `tfsdk:"pre_deployment_command_container"` + ManualWebhookSecretGithub types.String `tfsdk:"manual_webhook_secret_github"` + ManualWebhookSecretGitlab types.String `tfsdk:"manual_webhook_secret_gitlab"` + ManualWebhookSecretBitbucket types.String `tfsdk:"manual_webhook_secret_bitbucket"` + ManualWebhookSecretGitea types.String `tfsdk:"manual_webhook_secret_gitea"` + Redirect types.String `tfsdk:"redirect"` + UseBuildServer types.Bool `tfsdk:"use_build_server"` + IsHttpBasicAuthEnabled types.Bool `tfsdk:"is_http_basic_auth_enabled"` + HttpBasicAuthUsername types.String `tfsdk:"http_basic_auth_username"` + HttpBasicAuthPassword types.String `tfsdk:"http_basic_auth_password"` + DockerComposeLocation types.String `tfsdk:"docker_compose_location"` + DockerComposeCustomStartCommand types.String `tfsdk:"docker_compose_custom_start_command"` + DockerComposeCustomBuildCommand types.String `tfsdk:"docker_compose_custom_build_command"` + WatchPaths types.String `tfsdk:"watch_paths"` +} + +func (m ApplicationModel) Schema(ctx context.Context) schema.Schema { + return schema.Schema{ + Description: "Create, read, update, and delete a Coolify application resource.", + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + Computed: true, + Description: "UUID of the application.", + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "source_type": schema.StringAttribute{ + Required: true, + Description: "Type of application source. One of: public, private-github-app, private-deploy-key, dockerfile, dockerimage, dockercompose", + }, + "project_uuid": schema.StringAttribute{ + Required: true, + Description: "UUID of the project.", + }, + "server_uuid": schema.StringAttribute{ + Required: true, + Description: "UUID of the server.", + }, + "environment_name": schema.StringAttribute{ + Required: true, + Description: "Name of the environment.", + }, + "environment_uuid": schema.StringAttribute{ + Optional: true, + Description: "UUID of the environment. Will replace environment_name in future.", + }, + "destination_uuid": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "UUID of the destination if the server has multiple destinations.", + }, + "name": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Name of the application.", + }, + "description": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Description of the application.", + }, + "domains": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Application domains.", + }, + "instant_deploy": schema.BoolAttribute{ + Optional: true, + Computed: true, + Description: "Instant deploy the application.", + Default: booldefault.StaticBool(false), + }, + // Git-based fields + "git_repository": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Git repository URL. Required for public, private-github-app, and private-deploy-key source types.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "git_branch": schema.StringAttribute{ + Optional: true, + Description: "Git branch. Required for public, private-github-app, and private-deploy-key source types.", + }, + "build_pack": schema.StringAttribute{ + Optional: true, + Description: "Build pack type (nixpacks, static, dockerfile, dockercompose). Required for public, private-github-app, and private-deploy-key source types.", + }, + "ports_exposes": schema.StringAttribute{ + Optional: true, + Description: "Ports to expose. Required for public, private-github-app, private-deploy-key, and dockerimage source types.", + }, + // Private GitHub App specific + "github_app_uuid": schema.StringAttribute{ + Optional: true, + Description: "GitHub App UUID. Required for private-github-app source type.", + }, + // Private Deploy Key specific + "private_key_uuid": schema.StringAttribute{ + Optional: true, + Description: "Private key UUID. Required for private-deploy-key source type.", + }, + // Dockerfile specific + "dockerfile": schema.StringAttribute{ + Optional: true, + Description: "Dockerfile content. Required for dockerfile source type.", + }, + // Docker Image specific + "docker_registry_image_name": schema.StringAttribute{ + Optional: true, + Description: "Docker registry image name. Required for dockerimage source type.", + }, + "docker_registry_image_tag": schema.StringAttribute{ + Optional: true, + Description: "Docker registry image tag.", + }, + // Docker Compose specific + "docker_compose_raw": schema.StringAttribute{ + Optional: true, + Description: "Docker Compose raw content. Required for dockercompose source type.", + }, + // Optional common fields + "base_directory": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Base directory for all commands.", + PlanModifiers: []planmodifier.String{ + UseStateForUnknownUnlessNullString(), + }, + }, + "build_command": schema.StringAttribute{Optional: true, Description: "Build command."}, + "start_command": schema.StringAttribute{Optional: true, Description: "Start command."}, + "install_command": schema.StringAttribute{Optional: true, Description: "Install command."}, + "publish_directory": schema.StringAttribute{Optional: true, Description: "Publish directory."}, + "ports_mappings": schema.StringAttribute{Optional: true, Description: "Ports mappings."}, + "git_commit_sha": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Git commit SHA.", + PlanModifiers: []planmodifier.String{ + UseStateForUnknownUnlessNullString(), + }, + }, + "is_static": schema.BoolAttribute{Optional: true, Description: "Flag to indicate if the application is static."}, + "static_image": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Static image (e.g., nginx:alpine).", + PlanModifiers: []planmodifier.String{ + UseStateForUnknownUnlessNullString(), + }, + }, + "health_check_enabled": schema.BoolAttribute{Optional: true, Description: "Health check enabled."}, + "health_check_path": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Health check path.", + PlanModifiers: []planmodifier.String{ + UseStateForUnknownUnlessNullString(), + }, + }, + "health_check_port": schema.StringAttribute{Optional: true, Description: "Health check port."}, + "health_check_host": schema.StringAttribute{Optional: true, Description: "Health check host."}, + "health_check_method": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Health check method.", + PlanModifiers: []planmodifier.String{ + UseStateForUnknownUnlessNullString(), + }, + }, + "health_check_return_code": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Health check return code.", + PlanModifiers: []planmodifier.Int64{ + UseStateForUnknownUnlessNullInt64(), + }, + }, + "health_check_scheme": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Health check scheme.", + PlanModifiers: []planmodifier.String{ + UseStateForUnknownUnlessNullString(), + }, + }, + "health_check_response_text": schema.StringAttribute{Optional: true, Description: "Health check response text."}, + "health_check_interval": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Health check interval in seconds.", + PlanModifiers: []planmodifier.Int64{ + UseStateForUnknownUnlessNullInt64(), + }, + }, + "health_check_timeout": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Health check timeout in seconds.", + PlanModifiers: []planmodifier.Int64{ + UseStateForUnknownUnlessNullInt64(), + }, + }, + "health_check_retries": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Health check retries count.", + PlanModifiers: []planmodifier.Int64{ + UseStateForUnknownUnlessNullInt64(), + }, + }, + "health_check_start_period": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Health check start period in seconds.", + PlanModifiers: []planmodifier.Int64{ + UseStateForUnknownUnlessNullInt64(), + }, + }, + "limits_memory": schema.StringAttribute{Optional: true, Description: "Memory limit."}, + "limits_memory_swap": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Memory swap limit.", + PlanModifiers: []planmodifier.String{ + UseStateForUnknownUnlessNullString(), + }, + }, + "limits_memory_swappiness": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "Memory swappiness.", + PlanModifiers: []planmodifier.Int64{ + UseStateForUnknownUnlessNullInt64(), + }, + }, + "limits_memory_reservation": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Memory reservation.", + PlanModifiers: []planmodifier.String{ + UseStateForUnknownUnlessNullString(), + }, + }, + "limits_cpus": schema.StringAttribute{Optional: true, Description: "CPU limit."}, + "limits_cpuset": schema.StringAttribute{Optional: true, Description: "CPU set."}, + "limits_cpu_shares": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "CPU shares.", + PlanModifiers: []planmodifier.Int64{ + UseStateForUnknownUnlessNullInt64(), + }, + }, + "custom_labels": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Custom labels.", + PlanModifiers: []planmodifier.String{ + UseStateForUnknownUnlessNullString(), + }, + }, + "custom_docker_run_options": schema.StringAttribute{Optional: true, Description: "Custom docker run options."}, + "post_deployment_command": schema.StringAttribute{Optional: true, Description: "Post deployment command."}, + "post_deployment_command_container": schema.StringAttribute{Optional: true, Description: "Post deployment command container."}, + "pre_deployment_command": schema.StringAttribute{Optional: true, Description: "Pre deployment command."}, + "pre_deployment_command_container": schema.StringAttribute{Optional: true, Description: "Pre deployment command container."}, + "manual_webhook_secret_github": schema.StringAttribute{Optional: true, Sensitive: true, Description: "Manual webhook secret for Github."}, + "manual_webhook_secret_gitlab": schema.StringAttribute{Optional: true, Sensitive: true, Description: "Manual webhook secret for Gitlab."}, + "manual_webhook_secret_bitbucket": schema.StringAttribute{Optional: true, Sensitive: true, Description: "Manual webhook secret for Bitbucket."}, + "manual_webhook_secret_gitea": schema.StringAttribute{Optional: true, Sensitive: true, Description: "Manual webhook secret for Gitea."}, + "redirect": schema.StringAttribute{Optional: true, Description: "How to set redirect with Traefik / Caddy. www<->non-www."}, + "use_build_server": schema.BoolAttribute{Optional: true, Description: "Use build server."}, + "is_http_basic_auth_enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + Description: "HTTP Basic Authentication enabled.", + PlanModifiers: []planmodifier.Bool{ + UseStateForUnknownUnlessNullBool(), + }, + }, + "http_basic_auth_username": schema.StringAttribute{Optional: true, Description: "Username for HTTP Basic Authentication"}, + "http_basic_auth_password": schema.StringAttribute{Optional: true, Sensitive: true, Description: "Password for HTTP Basic Authentication"}, + "docker_compose_location": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "Docker Compose location.", + PlanModifiers: []planmodifier.String{ + UseStateForUnknownUnlessNullString(), + }, + }, + "docker_compose_custom_start_command": schema.StringAttribute{Optional: true, Description: "Docker Compose custom start command."}, + "docker_compose_custom_build_command": schema.StringAttribute{Optional: true, Description: "Docker Compose custom build command."}, + "watch_paths": schema.StringAttribute{Optional: true, Description: "Watch paths."}, + }, + } +} + +func preserveGitRepository(stateVal types.String, apiVal types.String) types.String { + if stateVal.IsNull() || stateVal.IsUnknown() { + return apiVal + } + stateStr := stateVal.ValueString() + apiStr := apiVal.ValueString() + if stateStr != "" && apiStr != "" && stateStr != apiStr { + if (strings.HasPrefix(stateStr, "http://") || strings.HasPrefix(stateStr, "https://") || strings.HasPrefix(stateStr, "git@")) && + !strings.Contains(apiStr, "://") && !strings.HasPrefix(apiStr, "git@") { + return stateVal + } + return stateVal + } + return apiVal +} + +func (m ApplicationModel) FromAPI(app *api.Application, state ApplicationModel) ApplicationModel { + apiGitRepo := flatten.String(app.GitRepository) + preservedGitRepo := preserveGitRepository(state.GitRepository, apiGitRepo) + + return ApplicationModel{ + Uuid: flatten.String(app.Uuid), + SourceType: state.SourceType, + ProjectUuid: state.ProjectUuid, + ServerUuid: state.ServerUuid, + EnvironmentName: state.EnvironmentName, + EnvironmentUuid: state.EnvironmentUuid, + DestinationUuid: state.DestinationUuid, + Name: flatten.String(app.Name), + Description: flatten.String(app.Description), + Domains: flatten.String(app.Fqdn), + InstantDeploy: state.InstantDeploy, + GitRepository: preservedGitRepo, + GitBranch: flatten.String(app.GitBranch), + BuildPack: flatten.String((*string)(app.BuildPack)), + PortsExposes: flatten.String(app.PortsExposes), + GithubAppUuid: state.GithubAppUuid, + PrivateKeyUuid: state.PrivateKeyUuid, + Dockerfile: flatten.String(app.Dockerfile), + DockerRegistryImageName: flatten.String(app.DockerRegistryImageName), + DockerRegistryImageTag: flatten.String(app.DockerRegistryImageTag), + DockerComposeRaw: flatten.String(app.DockerComposeRaw), + BaseDirectory: flatten.String(app.BaseDirectory), + BuildCommand: flatten.String(app.BuildCommand), + StartCommand: flatten.String(app.StartCommand), + InstallCommand: flatten.String(app.InstallCommand), + PublishDirectory: flatten.String(app.PublishDirectory), + PortsMappings: flatten.String(app.PortsMappings), + GitCommitSha: flatten.String(app.GitCommitSha), + IsStatic: state.IsStatic, + StaticImage: flatten.String(app.StaticImage), + HealthCheckEnabled: flatten.Bool(app.HealthCheckEnabled), + HealthCheckPath: flatten.String(app.HealthCheckPath), + HealthCheckPort: flatten.String(app.HealthCheckPort), + HealthCheckHost: flatten.String(app.HealthCheckHost), + HealthCheckMethod: flatten.String(app.HealthCheckMethod), + HealthCheckReturnCode: flatten.Int64(app.HealthCheckReturnCode), + HealthCheckScheme: flatten.String(app.HealthCheckScheme), + HealthCheckResponseText: flatten.String(app.HealthCheckResponseText), + HealthCheckInterval: flatten.Int64(app.HealthCheckInterval), + HealthCheckTimeout: flatten.Int64(app.HealthCheckTimeout), + HealthCheckRetries: flatten.Int64(app.HealthCheckRetries), + HealthCheckStartPeriod: flatten.Int64(app.HealthCheckStartPeriod), + LimitsMemory: flatten.String(app.LimitsMemory), + LimitsMemorySwap: flatten.String(app.LimitsMemorySwap), + LimitsMemorySwappiness: flatten.Int64(app.LimitsMemorySwappiness), + LimitsMemoryReservation: flatten.String(app.LimitsMemoryReservation), + LimitsCpus: flatten.String(app.LimitsCpus), + LimitsCpuset: flatten.String(app.LimitsCpuset), + LimitsCpuShares: flatten.Int64(app.LimitsCpuShares), + CustomLabels: flatten.String(app.CustomLabels), + CustomDockerRunOptions: flatten.String(app.CustomDockerRunOptions), + PostDeploymentCommand: flatten.String(app.PostDeploymentCommand), + PostDeploymentCommandContainer: flatten.String(app.PostDeploymentCommandContainer), + PreDeploymentCommand: flatten.String(app.PreDeploymentCommand), + PreDeploymentCommandContainer: flatten.String(app.PreDeploymentCommandContainer), + ManualWebhookSecretGithub: state.ManualWebhookSecretGithub, + ManualWebhookSecretGitlab: state.ManualWebhookSecretGitlab, + ManualWebhookSecretBitbucket: state.ManualWebhookSecretBitbucket, + ManualWebhookSecretGitea: state.ManualWebhookSecretGitea, + Redirect: flatten.String((*string)(app.Redirect)), + UseBuildServer: state.UseBuildServer, + IsHttpBasicAuthEnabled: flatten.Bool(app.IsHttpBasicAuthEnabled), + HttpBasicAuthUsername: flatten.String(app.HttpBasicAuthUsername), + HttpBasicAuthPassword: state.HttpBasicAuthPassword, + DockerComposeLocation: flatten.String(app.DockerComposeLocation), + DockerComposeCustomStartCommand: flatten.String(app.DockerComposeCustomStartCommand), + DockerComposeCustomBuildCommand: flatten.String(app.DockerComposeCustomBuildCommand), + WatchPaths: flatten.String(app.WatchPaths), + } +} + +// ToAPICreate routes to the appropriate API creation method based on source_type +func (m ApplicationModel) ToAPICreate() (interface{}, error) { + sourceType := ApplicationSourceType(m.SourceType.ValueString()) + + switch sourceType { + case ApplicationSourceTypePublic: + return m.toCreatePublicApplication(), nil + case ApplicationSourceTypePrivateGithubApp: + return m.toCreatePrivateGithubAppApplication(), nil + case ApplicationSourceTypePrivateDeployKey: + return m.toCreatePrivateDeployKeyApplication(), nil + case ApplicationSourceTypeDockerfile: + return m.toCreateDockerfileApplication(), nil + case ApplicationSourceTypeDockerimage: + return m.toCreateDockerimageApplication(), nil + case ApplicationSourceTypeDockercompose: + return m.toCreateDockercomposeApplication(), nil + default: + return nil, fmt.Errorf("unsupported source_type: %s", sourceType) + } +} + +func validateRedirect[T ~string](redirect *string) *T { + if redirect == nil || *redirect == "" { + return nil + } + validRedirects := map[string]bool{"both": true, "non-www": true, "www": true} + if !validRedirects[*redirect] { + return nil + } + enumVal := T(*redirect) + return &enumVal +} + +func (m ApplicationModel) toCreatePublicApplication() api.CreatePublicApplicationJSONRequestBody { + buildPack := api.CreatePublicApplicationJSONBodyBuildPack(m.BuildPack.ValueString()) + redirect := expand.StringOrNil(m.Redirect) + redirectEnum := validateRedirect[api.CreatePublicApplicationJSONBodyRedirect](redirect) + staticImage := expand.String(m.StaticImage) + var staticImageEnum *api.CreatePublicApplicationJSONBodyStaticImage + if staticImage != nil { + staticImageEnumVal := api.CreatePublicApplicationJSONBodyStaticImage(*staticImage) + staticImageEnum = &staticImageEnumVal + } + + return api.CreatePublicApplicationJSONRequestBody{ + ProjectUuid: m.ProjectUuid.ValueString(), + ServerUuid: m.ServerUuid.ValueString(), + EnvironmentName: m.EnvironmentName.ValueString(), + EnvironmentUuid: m.EnvironmentUuid.ValueString(), + GitRepository: m.GitRepository.ValueString(), + GitBranch: m.GitBranch.ValueString(), + BuildPack: buildPack, + PortsExposes: m.PortsExposes.ValueString(), + DestinationUuid: expand.StringOrNil(m.DestinationUuid), + Name: expand.String(m.Name), + Description: expand.String(m.Description), + Domains: expand.String(m.Domains), + GitCommitSha: expand.String(m.GitCommitSha), + DockerRegistryImageName: expand.String(m.DockerRegistryImageName), + DockerRegistryImageTag: expand.String(m.DockerRegistryImageTag), + IsStatic: expand.Bool(m.IsStatic), + StaticImage: staticImageEnum, + InstallCommand: expand.String(m.InstallCommand), + BuildCommand: expand.String(m.BuildCommand), + StartCommand: expand.String(m.StartCommand), + PortsMappings: expand.String(m.PortsMappings), + BaseDirectory: expand.String(m.BaseDirectory), + PublishDirectory: expand.String(m.PublishDirectory), + HealthCheckEnabled: expand.Bool(m.HealthCheckEnabled), + HealthCheckPath: expand.String(m.HealthCheckPath), + HealthCheckPort: expand.String(m.HealthCheckPort), + HealthCheckHost: expand.StringOrNil(m.HealthCheckHost), + HealthCheckMethod: expand.String(m.HealthCheckMethod), + HealthCheckReturnCode: expand.Int64(m.HealthCheckReturnCode), + HealthCheckScheme: expand.String(m.HealthCheckScheme), + HealthCheckResponseText: expand.String(m.HealthCheckResponseText), + HealthCheckInterval: expand.Int64(m.HealthCheckInterval), + HealthCheckTimeout: expand.Int64(m.HealthCheckTimeout), + HealthCheckRetries: expand.Int64(m.HealthCheckRetries), + HealthCheckStartPeriod: expand.Int64(m.HealthCheckStartPeriod), + LimitsMemory: expand.String(m.LimitsMemory), + LimitsMemorySwap: expand.String(m.LimitsMemorySwap), + LimitsMemorySwappiness: expand.Int64(m.LimitsMemorySwappiness), + LimitsMemoryReservation: expand.String(m.LimitsMemoryReservation), + LimitsCpus: expand.String(m.LimitsCpus), + LimitsCpuset: expand.String(m.LimitsCpuset), + LimitsCpuShares: expand.Int64(m.LimitsCpuShares), + CustomLabels: expand.String(m.CustomLabels), + CustomDockerRunOptions: expand.String(m.CustomDockerRunOptions), + PostDeploymentCommand: expand.String(m.PostDeploymentCommand), + PostDeploymentCommandContainer: expand.String(m.PostDeploymentCommandContainer), + PreDeploymentCommand: expand.String(m.PreDeploymentCommand), + PreDeploymentCommandContainer: expand.String(m.PreDeploymentCommandContainer), + ManualWebhookSecretGithub: expand.String(m.ManualWebhookSecretGithub), + ManualWebhookSecretGitlab: expand.String(m.ManualWebhookSecretGitlab), + ManualWebhookSecretBitbucket: expand.String(m.ManualWebhookSecretBitbucket), + ManualWebhookSecretGitea: expand.String(m.ManualWebhookSecretGitea), + Redirect: redirectEnum, + InstantDeploy: expand.Bool(m.InstantDeploy), + Dockerfile: expand.String(m.Dockerfile), + DockerComposeLocation: expand.String(m.DockerComposeLocation), + DockerComposeRaw: expand.String(m.DockerComposeRaw), + DockerComposeCustomStartCommand: expand.String(m.DockerComposeCustomStartCommand), + DockerComposeCustomBuildCommand: expand.String(m.DockerComposeCustomBuildCommand), + WatchPaths: expand.String(m.WatchPaths), + UseBuildServer: expand.Bool(m.UseBuildServer), + IsHttpBasicAuthEnabled: expand.Bool(m.IsHttpBasicAuthEnabled), + HttpBasicAuthUsername: expand.String(m.HttpBasicAuthUsername), + HttpBasicAuthPassword: expand.String(m.HttpBasicAuthPassword), + } +} + +func (m ApplicationModel) toCreatePrivateGithubAppApplication() api.CreatePrivateGithubAppApplicationJSONRequestBody { + buildPack := api.CreatePrivateGithubAppApplicationJSONBodyBuildPack(m.BuildPack.ValueString()) + redirect := expand.StringOrNil(m.Redirect) + redirectEnum := validateRedirect[api.CreatePrivateGithubAppApplicationJSONBodyRedirect](redirect) + staticImage := expand.String(m.StaticImage) + var staticImageEnum *api.CreatePrivateGithubAppApplicationJSONBodyStaticImage + if staticImage != nil { + staticImageEnumVal := api.CreatePrivateGithubAppApplicationJSONBodyStaticImage(*staticImage) + staticImageEnum = &staticImageEnumVal + } + + return api.CreatePrivateGithubAppApplicationJSONRequestBody{ + ProjectUuid: m.ProjectUuid.ValueString(), + ServerUuid: m.ServerUuid.ValueString(), + EnvironmentName: m.EnvironmentName.ValueString(), + EnvironmentUuid: m.EnvironmentUuid.ValueString(), + GithubAppUuid: m.GithubAppUuid.ValueString(), + GitRepository: m.GitRepository.ValueString(), + GitBranch: m.GitBranch.ValueString(), + BuildPack: buildPack, + PortsExposes: m.PortsExposes.ValueString(), + DestinationUuid: expand.StringOrNil(m.DestinationUuid), + Name: expand.String(m.Name), + Description: expand.String(m.Description), + Domains: expand.String(m.Domains), + GitCommitSha: expand.String(m.GitCommitSha), + DockerRegistryImageName: expand.String(m.DockerRegistryImageName), + DockerRegistryImageTag: expand.String(m.DockerRegistryImageTag), + IsStatic: expand.Bool(m.IsStatic), + StaticImage: staticImageEnum, + InstallCommand: expand.String(m.InstallCommand), + BuildCommand: expand.String(m.BuildCommand), + StartCommand: expand.String(m.StartCommand), + PortsMappings: expand.String(m.PortsMappings), + BaseDirectory: expand.String(m.BaseDirectory), + PublishDirectory: expand.String(m.PublishDirectory), + HealthCheckEnabled: expand.Bool(m.HealthCheckEnabled), + HealthCheckPath: expand.String(m.HealthCheckPath), + HealthCheckPort: expand.String(m.HealthCheckPort), + HealthCheckHost: expand.StringOrNil(m.HealthCheckHost), + HealthCheckMethod: expand.String(m.HealthCheckMethod), + HealthCheckReturnCode: expand.Int64(m.HealthCheckReturnCode), + HealthCheckScheme: expand.String(m.HealthCheckScheme), + HealthCheckResponseText: expand.String(m.HealthCheckResponseText), + HealthCheckInterval: expand.Int64(m.HealthCheckInterval), + HealthCheckTimeout: expand.Int64(m.HealthCheckTimeout), + HealthCheckRetries: expand.Int64(m.HealthCheckRetries), + HealthCheckStartPeriod: expand.Int64(m.HealthCheckStartPeriod), + LimitsMemory: expand.String(m.LimitsMemory), + LimitsMemorySwap: expand.String(m.LimitsMemorySwap), + LimitsMemorySwappiness: expand.Int64(m.LimitsMemorySwappiness), + LimitsMemoryReservation: expand.String(m.LimitsMemoryReservation), + LimitsCpus: expand.String(m.LimitsCpus), + LimitsCpuset: expand.String(m.LimitsCpuset), + LimitsCpuShares: expand.Int64(m.LimitsCpuShares), + CustomLabels: expand.String(m.CustomLabels), + CustomDockerRunOptions: expand.String(m.CustomDockerRunOptions), + PostDeploymentCommand: expand.String(m.PostDeploymentCommand), + PostDeploymentCommandContainer: expand.String(m.PostDeploymentCommandContainer), + PreDeploymentCommand: expand.String(m.PreDeploymentCommand), + PreDeploymentCommandContainer: expand.String(m.PreDeploymentCommandContainer), + ManualWebhookSecretGithub: expand.String(m.ManualWebhookSecretGithub), + ManualWebhookSecretGitlab: expand.String(m.ManualWebhookSecretGitlab), + ManualWebhookSecretBitbucket: expand.String(m.ManualWebhookSecretBitbucket), + ManualWebhookSecretGitea: expand.String(m.ManualWebhookSecretGitea), + Redirect: redirectEnum, + InstantDeploy: expand.Bool(m.InstantDeploy), + Dockerfile: expand.String(m.Dockerfile), + DockerComposeLocation: expand.String(m.DockerComposeLocation), + DockerComposeRaw: expand.String(m.DockerComposeRaw), + DockerComposeCustomStartCommand: expand.String(m.DockerComposeCustomStartCommand), + DockerComposeCustomBuildCommand: expand.String(m.DockerComposeCustomBuildCommand), + WatchPaths: expand.String(m.WatchPaths), + UseBuildServer: expand.Bool(m.UseBuildServer), + IsHttpBasicAuthEnabled: expand.Bool(m.IsHttpBasicAuthEnabled), + HttpBasicAuthUsername: expand.String(m.HttpBasicAuthUsername), + HttpBasicAuthPassword: expand.String(m.HttpBasicAuthPassword), + } +} + +func (m ApplicationModel) toCreatePrivateDeployKeyApplication() api.CreatePrivateDeployKeyApplicationJSONRequestBody { + buildPack := api.CreatePrivateDeployKeyApplicationJSONBodyBuildPack(m.BuildPack.ValueString()) + redirect := expand.StringOrNil(m.Redirect) + redirectEnum := validateRedirect[api.CreatePrivateDeployKeyApplicationJSONBodyRedirect](redirect) + staticImage := expand.String(m.StaticImage) + var staticImageEnum *api.CreatePrivateDeployKeyApplicationJSONBodyStaticImage + if staticImage != nil { + staticImageEnumVal := api.CreatePrivateDeployKeyApplicationJSONBodyStaticImage(*staticImage) + staticImageEnum = &staticImageEnumVal + } + + return api.CreatePrivateDeployKeyApplicationJSONRequestBody{ + ProjectUuid: m.ProjectUuid.ValueString(), + ServerUuid: m.ServerUuid.ValueString(), + EnvironmentName: m.EnvironmentName.ValueString(), + EnvironmentUuid: m.EnvironmentUuid.ValueString(), + PrivateKeyUuid: m.PrivateKeyUuid.ValueString(), + GitRepository: m.GitRepository.ValueString(), + GitBranch: m.GitBranch.ValueString(), + BuildPack: buildPack, + PortsExposes: m.PortsExposes.ValueString(), + DestinationUuid: expand.StringOrNil(m.DestinationUuid), + Name: expand.String(m.Name), + Description: expand.String(m.Description), + Domains: expand.String(m.Domains), + GitCommitSha: expand.String(m.GitCommitSha), + DockerRegistryImageName: expand.String(m.DockerRegistryImageName), + DockerRegistryImageTag: expand.String(m.DockerRegistryImageTag), + IsStatic: expand.Bool(m.IsStatic), + StaticImage: staticImageEnum, + InstallCommand: expand.String(m.InstallCommand), + BuildCommand: expand.String(m.BuildCommand), + StartCommand: expand.String(m.StartCommand), + PortsMappings: expand.String(m.PortsMappings), + BaseDirectory: expand.String(m.BaseDirectory), + PublishDirectory: expand.String(m.PublishDirectory), + HealthCheckEnabled: expand.Bool(m.HealthCheckEnabled), + HealthCheckPath: expand.String(m.HealthCheckPath), + HealthCheckPort: expand.String(m.HealthCheckPort), + HealthCheckHost: expand.StringOrNil(m.HealthCheckHost), + HealthCheckMethod: expand.String(m.HealthCheckMethod), + HealthCheckReturnCode: expand.Int64(m.HealthCheckReturnCode), + HealthCheckScheme: expand.String(m.HealthCheckScheme), + HealthCheckResponseText: expand.String(m.HealthCheckResponseText), + HealthCheckInterval: expand.Int64(m.HealthCheckInterval), + HealthCheckTimeout: expand.Int64(m.HealthCheckTimeout), + HealthCheckRetries: expand.Int64(m.HealthCheckRetries), + HealthCheckStartPeriod: expand.Int64(m.HealthCheckStartPeriod), + LimitsMemory: expand.String(m.LimitsMemory), + LimitsMemorySwap: expand.String(m.LimitsMemorySwap), + LimitsMemorySwappiness: expand.Int64(m.LimitsMemorySwappiness), + LimitsMemoryReservation: expand.String(m.LimitsMemoryReservation), + LimitsCpus: expand.String(m.LimitsCpus), + LimitsCpuset: expand.String(m.LimitsCpuset), + LimitsCpuShares: expand.Int64(m.LimitsCpuShares), + CustomLabels: expand.String(m.CustomLabels), + CustomDockerRunOptions: expand.String(m.CustomDockerRunOptions), + PostDeploymentCommand: expand.String(m.PostDeploymentCommand), + PostDeploymentCommandContainer: expand.String(m.PostDeploymentCommandContainer), + PreDeploymentCommand: expand.String(m.PreDeploymentCommand), + PreDeploymentCommandContainer: expand.String(m.PreDeploymentCommandContainer), + ManualWebhookSecretGithub: expand.String(m.ManualWebhookSecretGithub), + ManualWebhookSecretGitlab: expand.String(m.ManualWebhookSecretGitlab), + ManualWebhookSecretBitbucket: expand.String(m.ManualWebhookSecretBitbucket), + ManualWebhookSecretGitea: expand.String(m.ManualWebhookSecretGitea), + Redirect: redirectEnum, + InstantDeploy: expand.Bool(m.InstantDeploy), + Dockerfile: expand.String(m.Dockerfile), + DockerComposeLocation: expand.String(m.DockerComposeLocation), + DockerComposeRaw: expand.String(m.DockerComposeRaw), + DockerComposeCustomStartCommand: expand.String(m.DockerComposeCustomStartCommand), + DockerComposeCustomBuildCommand: expand.String(m.DockerComposeCustomBuildCommand), + WatchPaths: expand.String(m.WatchPaths), + UseBuildServer: expand.Bool(m.UseBuildServer), + IsHttpBasicAuthEnabled: expand.Bool(m.IsHttpBasicAuthEnabled), + HttpBasicAuthUsername: expand.String(m.HttpBasicAuthUsername), + HttpBasicAuthPassword: expand.String(m.HttpBasicAuthPassword), + } +} + +func (m ApplicationModel) toCreateDockerfileApplication() api.CreateDockerfileApplicationJSONRequestBody { + buildPack := expand.String(m.BuildPack) + var buildPackEnum *api.CreateDockerfileApplicationJSONBodyBuildPack + if buildPack != nil { + buildPackEnumVal := api.CreateDockerfileApplicationJSONBodyBuildPack(*buildPack) + buildPackEnum = &buildPackEnumVal + } + redirect := expand.StringOrNil(m.Redirect) + redirectEnum := validateRedirect[api.CreateDockerfileApplicationJSONBodyRedirect](redirect) + + return api.CreateDockerfileApplicationJSONRequestBody{ + ProjectUuid: m.ProjectUuid.ValueString(), + ServerUuid: m.ServerUuid.ValueString(), + EnvironmentName: m.EnvironmentName.ValueString(), + EnvironmentUuid: m.EnvironmentUuid.ValueString(), + Dockerfile: m.Dockerfile.ValueString(), + BuildPack: buildPackEnum, + PortsExposes: expand.String(m.PortsExposes), + DestinationUuid: expand.StringOrNil(m.DestinationUuid), + Name: expand.String(m.Name), + Description: expand.String(m.Description), + Domains: expand.String(m.Domains), + DockerRegistryImageName: expand.String(m.DockerRegistryImageName), + DockerRegistryImageTag: expand.String(m.DockerRegistryImageTag), + PortsMappings: expand.String(m.PortsMappings), + BaseDirectory: expand.String(m.BaseDirectory), + HealthCheckEnabled: expand.Bool(m.HealthCheckEnabled), + HealthCheckPath: expand.String(m.HealthCheckPath), + HealthCheckPort: expand.String(m.HealthCheckPort), + HealthCheckHost: expand.StringOrNil(m.HealthCheckHost), + HealthCheckMethod: expand.String(m.HealthCheckMethod), + HealthCheckReturnCode: expand.Int64(m.HealthCheckReturnCode), + HealthCheckScheme: expand.String(m.HealthCheckScheme), + HealthCheckResponseText: expand.String(m.HealthCheckResponseText), + HealthCheckInterval: expand.Int64(m.HealthCheckInterval), + HealthCheckTimeout: expand.Int64(m.HealthCheckTimeout), + HealthCheckRetries: expand.Int64(m.HealthCheckRetries), + HealthCheckStartPeriod: expand.Int64(m.HealthCheckStartPeriod), + LimitsMemory: expand.String(m.LimitsMemory), + LimitsMemorySwap: expand.String(m.LimitsMemorySwap), + LimitsMemorySwappiness: expand.Int64(m.LimitsMemorySwappiness), + LimitsMemoryReservation: expand.String(m.LimitsMemoryReservation), + LimitsCpus: expand.String(m.LimitsCpus), + LimitsCpuset: expand.String(m.LimitsCpuset), + LimitsCpuShares: expand.Int64(m.LimitsCpuShares), + CustomLabels: expand.String(m.CustomLabels), + CustomDockerRunOptions: expand.String(m.CustomDockerRunOptions), + PostDeploymentCommand: expand.String(m.PostDeploymentCommand), + PostDeploymentCommandContainer: expand.String(m.PostDeploymentCommandContainer), + PreDeploymentCommand: expand.String(m.PreDeploymentCommand), + PreDeploymentCommandContainer: expand.String(m.PreDeploymentCommandContainer), + ManualWebhookSecretGithub: expand.String(m.ManualWebhookSecretGithub), + ManualWebhookSecretGitlab: expand.String(m.ManualWebhookSecretGitlab), + ManualWebhookSecretBitbucket: expand.String(m.ManualWebhookSecretBitbucket), + ManualWebhookSecretGitea: expand.String(m.ManualWebhookSecretGitea), + Redirect: redirectEnum, + InstantDeploy: expand.Bool(m.InstantDeploy), + UseBuildServer: expand.Bool(m.UseBuildServer), + } +} + +func (m ApplicationModel) toCreateDockerimageApplication() api.CreateDockerimageApplicationJSONRequestBody { + redirect := expand.StringOrNil(m.Redirect) + redirectEnum := validateRedirect[api.CreateDockerimageApplicationJSONBodyRedirect](redirect) + + return api.CreateDockerimageApplicationJSONRequestBody{ + ProjectUuid: m.ProjectUuid.ValueString(), + ServerUuid: m.ServerUuid.ValueString(), + EnvironmentName: m.EnvironmentName.ValueString(), + EnvironmentUuid: m.EnvironmentUuid.ValueString(), + DockerRegistryImageName: m.DockerRegistryImageName.ValueString(), + DockerRegistryImageTag: expand.String(m.DockerRegistryImageTag), + PortsExposes: m.PortsExposes.ValueString(), + DestinationUuid: expand.StringOrNil(m.DestinationUuid), + Name: expand.String(m.Name), + Description: expand.String(m.Description), + Domains: expand.String(m.Domains), + PortsMappings: expand.String(m.PortsMappings), + HealthCheckEnabled: expand.Bool(m.HealthCheckEnabled), + HealthCheckPath: expand.String(m.HealthCheckPath), + HealthCheckPort: expand.String(m.HealthCheckPort), + HealthCheckHost: expand.StringOrNil(m.HealthCheckHost), + HealthCheckMethod: expand.String(m.HealthCheckMethod), + HealthCheckReturnCode: expand.Int64(m.HealthCheckReturnCode), + HealthCheckScheme: expand.String(m.HealthCheckScheme), + HealthCheckResponseText: expand.String(m.HealthCheckResponseText), + HealthCheckInterval: expand.Int64(m.HealthCheckInterval), + HealthCheckTimeout: expand.Int64(m.HealthCheckTimeout), + HealthCheckRetries: expand.Int64(m.HealthCheckRetries), + HealthCheckStartPeriod: expand.Int64(m.HealthCheckStartPeriod), + LimitsMemory: expand.String(m.LimitsMemory), + LimitsMemorySwap: expand.String(m.LimitsMemorySwap), + LimitsMemorySwappiness: expand.Int64(m.LimitsMemorySwappiness), + LimitsMemoryReservation: expand.String(m.LimitsMemoryReservation), + LimitsCpus: expand.String(m.LimitsCpus), + LimitsCpuset: expand.String(m.LimitsCpuset), + LimitsCpuShares: expand.Int64(m.LimitsCpuShares), + CustomLabels: expand.String(m.CustomLabels), + CustomDockerRunOptions: expand.String(m.CustomDockerRunOptions), + PostDeploymentCommand: expand.String(m.PostDeploymentCommand), + PostDeploymentCommandContainer: expand.String(m.PostDeploymentCommandContainer), + PreDeploymentCommand: expand.String(m.PreDeploymentCommand), + PreDeploymentCommandContainer: expand.String(m.PreDeploymentCommandContainer), + ManualWebhookSecretGithub: expand.String(m.ManualWebhookSecretGithub), + ManualWebhookSecretGitlab: expand.String(m.ManualWebhookSecretGitlab), + ManualWebhookSecretBitbucket: expand.String(m.ManualWebhookSecretBitbucket), + ManualWebhookSecretGitea: expand.String(m.ManualWebhookSecretGitea), + Redirect: redirectEnum, + InstantDeploy: expand.Bool(m.InstantDeploy), + } +} + +func (m ApplicationModel) toCreateDockercomposeApplication() api.CreateDockercomposeApplicationJSONRequestBody { + return api.CreateDockercomposeApplicationJSONRequestBody{ + ProjectUuid: m.ProjectUuid.ValueString(), + ServerUuid: m.ServerUuid.ValueString(), + EnvironmentName: m.EnvironmentName.ValueString(), + EnvironmentUuid: m.EnvironmentUuid.ValueString(), + DockerComposeRaw: m.DockerComposeRaw.ValueString(), + DestinationUuid: expand.StringOrNil(m.DestinationUuid), + Name: expand.String(m.Name), + Description: expand.String(m.Description), + InstantDeploy: expand.Bool(m.InstantDeploy), + UseBuildServer: expand.Bool(m.UseBuildServer), + } +} + +func (m ApplicationModel) ToAPIUpdate() api.UpdateApplicationByUuidJSONRequestBody { + buildPack := expand.String(m.BuildPack) + var buildPackEnum *api.UpdateApplicationByUuidJSONBodyBuildPack + if buildPack != nil { + buildPackEnumVal := api.UpdateApplicationByUuidJSONBodyBuildPack(*buildPack) + buildPackEnum = &buildPackEnumVal + } + redirect := expand.StringOrNil(m.Redirect) + redirectEnum := validateRedirect[api.UpdateApplicationByUuidJSONBodyRedirect](redirect) + + return api.UpdateApplicationByUuidJSONRequestBody{ + Description: expand.String(m.Description), + DestinationUuid: expand.StringOrNil(m.DestinationUuid), + Domains: expand.String(m.Domains), + Name: expand.String(m.Name), + ProjectUuid: expand.String(m.ProjectUuid), + ServerUuid: expand.String(m.ServerUuid), + EnvironmentName: expand.String(m.EnvironmentName), + BuildPack: buildPackEnum, + BaseDirectory: expand.String(m.BaseDirectory), + BuildCommand: expand.String(m.BuildCommand), + StartCommand: expand.String(m.StartCommand), + InstallCommand: expand.String(m.InstallCommand), + PublishDirectory: expand.String(m.PublishDirectory), + PortsMappings: expand.String(m.PortsMappings), + PortsExposes: expand.String(m.PortsExposes), + GitCommitSha: expand.String(m.GitCommitSha), + GitBranch: expand.String(m.GitBranch), + GitRepository: expand.String(m.GitRepository), + GithubAppUuid: expand.String(m.GithubAppUuid), + HealthCheckEnabled: expand.Bool(m.HealthCheckEnabled), + HealthCheckPath: expand.String(m.HealthCheckPath), + HealthCheckPort: expand.String(m.HealthCheckPort), + HealthCheckHost: expand.StringOrNil(m.HealthCheckHost), + HealthCheckMethod: expand.String(m.HealthCheckMethod), + HealthCheckReturnCode: expand.Int64(m.HealthCheckReturnCode), + HealthCheckScheme: expand.String(m.HealthCheckScheme), + HealthCheckResponseText: expand.String(m.HealthCheckResponseText), + HealthCheckInterval: expand.Int64(m.HealthCheckInterval), + HealthCheckTimeout: expand.Int64(m.HealthCheckTimeout), + HealthCheckRetries: expand.Int64(m.HealthCheckRetries), + HealthCheckStartPeriod: expand.Int64(m.HealthCheckStartPeriod), + LimitsMemory: expand.String(m.LimitsMemory), + LimitsMemorySwap: expand.String(m.LimitsMemorySwap), + LimitsMemorySwappiness: expand.Int64(m.LimitsMemorySwappiness), + LimitsMemoryReservation: expand.String(m.LimitsMemoryReservation), + LimitsCpus: expand.String(m.LimitsCpus), + LimitsCpuset: expand.String(m.LimitsCpuset), + LimitsCpuShares: expand.Int64(m.LimitsCpuShares), + CustomLabels: expand.String(m.CustomLabels), + CustomDockerRunOptions: expand.String(m.CustomDockerRunOptions), + PostDeploymentCommand: expand.String(m.PostDeploymentCommand), + PostDeploymentCommandContainer: expand.String(m.PostDeploymentCommandContainer), + PreDeploymentCommand: expand.String(m.PreDeploymentCommand), + PreDeploymentCommandContainer: expand.String(m.PreDeploymentCommandContainer), + ManualWebhookSecretGithub: expand.String(m.ManualWebhookSecretGithub), + ManualWebhookSecretGitlab: expand.String(m.ManualWebhookSecretGitlab), + ManualWebhookSecretBitbucket: expand.String(m.ManualWebhookSecretBitbucket), + ManualWebhookSecretGitea: expand.String(m.ManualWebhookSecretGitea), + Redirect: redirectEnum, + InstantDeploy: expand.Bool(m.InstantDeploy), + Dockerfile: expand.String(m.Dockerfile), + DockerComposeLocation: expand.String(m.DockerComposeLocation), + DockerComposeRaw: expand.String(m.DockerComposeRaw), + DockerComposeCustomStartCommand: expand.String(m.DockerComposeCustomStartCommand), + DockerComposeCustomBuildCommand: expand.String(m.DockerComposeCustomBuildCommand), + DockerComposeDomains: nil, + WatchPaths: expand.String(m.WatchPaths), + UseBuildServer: expand.Bool(m.UseBuildServer), + DockerRegistryImageName: expand.String(m.DockerRegistryImageName), + DockerRegistryImageTag: expand.String(m.DockerRegistryImageTag), + } +} + diff --git a/internal/service/application_planmodifier.go b/internal/service/application_planmodifier.go new file mode 100644 index 0000000..8e82502 --- /dev/null +++ b/internal/service/application_planmodifier.go @@ -0,0 +1,102 @@ +package service + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type useStateForUnknownUnlessNull struct{} + +func (m useStateForUnknownUnlessNull) Description(ctx context.Context) string { + return "Handles Optional+Computed fields: marks as Unknown on create when null, preserves state on update" +} + +func (m useStateForUnknownUnlessNull) MarkdownDescription(ctx context.Context) string { + return "Handles Optional+Computed fields: marks as Unknown on create when null, preserves state on update" +} + +func (m useStateForUnknownUnlessNull) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { + if !req.ConfigValue.IsNull() { + return + } + + if !req.PlanValue.IsNull() { + return + } + + if req.StateValue.IsNull() || req.StateValue.IsUnknown() { + resp.PlanValue = types.StringUnknown() + return + } + + resp.PlanValue = req.StateValue +} + +func UseStateForUnknownUnlessNullString() planmodifier.String { + return useStateForUnknownUnlessNull{} +} + +type useStateForUnknownUnlessNullInt64 struct{} + +func (m useStateForUnknownUnlessNullInt64) Description(ctx context.Context) string { + return "Handles Optional+Computed fields: marks as Unknown on create when null, preserves state on update" +} + +func (m useStateForUnknownUnlessNullInt64) MarkdownDescription(ctx context.Context) string { + return "Handles Optional+Computed fields: marks as Unknown on create when null, preserves state on update" +} + +func (m useStateForUnknownUnlessNullInt64) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { + if !req.ConfigValue.IsNull() { + return + } + + if !req.PlanValue.IsNull() { + return + } + + if req.StateValue.IsNull() || req.StateValue.IsUnknown() { + resp.PlanValue = types.Int64Unknown() + return + } + + resp.PlanValue = req.StateValue +} + +func UseStateForUnknownUnlessNullInt64() planmodifier.Int64 { + return useStateForUnknownUnlessNullInt64{} +} + +type useStateForUnknownUnlessNullBool struct{} + +func (m useStateForUnknownUnlessNullBool) Description(ctx context.Context) string { + return "Handles Optional+Computed fields: marks as Unknown on create when null, preserves state on update" +} + +func (m useStateForUnknownUnlessNullBool) MarkdownDescription(ctx context.Context) string { + return "Handles Optional+Computed fields: marks as Unknown on create when null, preserves state on update" +} + +func (m useStateForUnknownUnlessNullBool) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) { + if !req.ConfigValue.IsNull() { + return + } + + if !req.PlanValue.IsNull() { + return + } + + if req.StateValue.IsNull() || req.StateValue.IsUnknown() { + resp.PlanValue = types.BoolUnknown() + return + } + + resp.PlanValue = req.StateValue +} + +func UseStateForUnknownUnlessNullBool() planmodifier.Bool { + return useStateForUnknownUnlessNullBool{} +} + diff --git a/internal/service/application_resource.go b/internal/service/application_resource.go new file mode 100644 index 0000000..167c833 --- /dev/null +++ b/internal/service/application_resource.go @@ -0,0 +1,480 @@ +package service + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "terraform-provider-coolify/internal/api" + "terraform-provider-coolify/internal/provider/util" +) + +var ( + _ resource.Resource = &applicationResource{} + _ resource.ResourceWithConfigure = &applicationResource{} + _ resource.ResourceWithImportState = &applicationResource{} +) + +func NewApplicationResource() resource.Resource { + return &applicationResource{} +} + +type applicationResource struct { + client *api.ClientWithResponses +} + +func (r *applicationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_application" +} + +func (r *applicationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = ApplicationModel{}.Schema(ctx) + resp.Schema.Description = "Create, read, update, and delete a Coolify application resource." + + sensitiveAttrs := []string{ + "manual_webhook_secret_bitbucket", + "manual_webhook_secret_gitea", + "manual_webhook_secret_github", + "manual_webhook_secret_gitlab", + "http_basic_auth_password", + } + for _, attr := range sensitiveAttrs { + if err := makeResourceAttributeSensitive(resp.Schema.Attributes, attr); err != nil { + tflog.Warn(ctx, fmt.Sprintf("Failed to mark attribute as sensitive: %s", attr)) + } + } +} + +func (r *applicationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + util.ProviderDataFromResourceConfigureRequest(req, &r.client, resp) +} + +func (r *applicationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan ApplicationModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if diags := r.validateCreatePlan(ctx, plan); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + sourceType := ApplicationSourceType(plan.SourceType.ValueString()) + tflog.Debug(ctx, "Creating application", map[string]interface{}{ + "source_type": sourceType, + "name": plan.Name.ValueString(), + }) + + createBody, err := plan.ToAPICreate() + if err != nil { + resp.Diagnostics.AddError( + "Error preparing application creation", + err.Error(), + ) + return + } + + var uuid string + switch sourceType { + case ApplicationSourceTypePublic: + body := createBody.(api.CreatePublicApplicationJSONRequestBody) + apiResp, err := r.client.CreatePublicApplicationWithResponse(ctx, body) + if err != nil { + resp.Diagnostics.AddError("Error creating application", err.Error()) + return + } + if apiResp.StatusCode() != http.StatusCreated { + resp.Diagnostics.AddError( + "Unexpected HTTP status code creating application", + fmt.Sprintf("Received %d creating application. Details: %s", apiResp.StatusCode(), string(apiResp.Body)), + ) + return + } + if apiResp.JSON201 == nil || apiResp.JSON201.Uuid == nil { + resp.Diagnostics.AddError( + "Invalid response creating application", + "Response did not contain application UUID", + ) + return + } + uuid = *apiResp.JSON201.Uuid + case ApplicationSourceTypePrivateGithubApp: + body := createBody.(api.CreatePrivateGithubAppApplicationJSONRequestBody) + apiResp, err := r.client.CreatePrivateGithubAppApplicationWithResponse(ctx, body) + if err != nil { + resp.Diagnostics.AddError("Error creating application", err.Error()) + return + } + if apiResp.StatusCode() != http.StatusCreated { + resp.Diagnostics.AddError( + "Unexpected HTTP status code creating application", + fmt.Sprintf("Received %d creating application. Details: %s", apiResp.StatusCode(), string(apiResp.Body)), + ) + return + } + if apiResp.JSON201 == nil || apiResp.JSON201.Uuid == nil { + resp.Diagnostics.AddError( + "Invalid response creating application", + "Response did not contain application UUID", + ) + return + } + uuid = *apiResp.JSON201.Uuid + case ApplicationSourceTypePrivateDeployKey: + body := createBody.(api.CreatePrivateDeployKeyApplicationJSONRequestBody) + apiResp, err := r.client.CreatePrivateDeployKeyApplicationWithResponse(ctx, body) + if err != nil { + resp.Diagnostics.AddError("Error creating application", err.Error()) + return + } + if apiResp.StatusCode() != http.StatusCreated { + resp.Diagnostics.AddError( + "Unexpected HTTP status code creating application", + fmt.Sprintf("Received %d creating application. Details: %s", apiResp.StatusCode(), string(apiResp.Body)), + ) + return + } + if apiResp.JSON201 == nil || apiResp.JSON201.Uuid == nil { + resp.Diagnostics.AddError( + "Invalid response creating application", + "Response did not contain application UUID", + ) + return + } + uuid = *apiResp.JSON201.Uuid + case ApplicationSourceTypeDockerfile: + body := createBody.(api.CreateDockerfileApplicationJSONRequestBody) + apiResp, err := r.client.CreateDockerfileApplicationWithResponse(ctx, body) + if err != nil { + resp.Diagnostics.AddError("Error creating application", err.Error()) + return + } + if apiResp.StatusCode() != http.StatusCreated { + resp.Diagnostics.AddError( + "Unexpected HTTP status code creating application", + fmt.Sprintf("Received %d creating application. Details: %s", apiResp.StatusCode(), string(apiResp.Body)), + ) + return + } + if apiResp.JSON201 == nil || apiResp.JSON201.Uuid == nil { + resp.Diagnostics.AddError( + "Invalid response creating application", + "Response did not contain application UUID", + ) + return + } + uuid = *apiResp.JSON201.Uuid + case ApplicationSourceTypeDockerimage: + body := createBody.(api.CreateDockerimageApplicationJSONRequestBody) + apiResp, err := r.client.CreateDockerimageApplicationWithResponse(ctx, body) + if err != nil { + resp.Diagnostics.AddError("Error creating application", err.Error()) + return + } + if apiResp.StatusCode() != http.StatusCreated { + resp.Diagnostics.AddError( + "Unexpected HTTP status code creating application", + fmt.Sprintf("Received %d creating application. Details: %s", apiResp.StatusCode(), string(apiResp.Body)), + ) + return + } + if apiResp.JSON201 == nil || apiResp.JSON201.Uuid == nil { + resp.Diagnostics.AddError( + "Invalid response creating application", + "Response did not contain application UUID", + ) + return + } + uuid = *apiResp.JSON201.Uuid + case ApplicationSourceTypeDockercompose: + body := createBody.(api.CreateDockercomposeApplicationJSONRequestBody) + apiResp, err := r.client.CreateDockercomposeApplicationWithResponse(ctx, body) + if err != nil { + resp.Diagnostics.AddError("Error creating application", err.Error()) + return + } + if apiResp.StatusCode() != http.StatusCreated { + resp.Diagnostics.AddError( + "Unexpected HTTP status code creating application", + fmt.Sprintf("Received %d creating application. Details: %s", apiResp.StatusCode(), string(apiResp.Body)), + ) + return + } + if apiResp.JSON201 == nil || apiResp.JSON201.Uuid == nil { + resp.Diagnostics.AddError( + "Invalid response creating application", + "Response did not contain application UUID", + ) + return + } + uuid = *apiResp.JSON201.Uuid + default: + resp.Diagnostics.AddError( + "Unsupported source_type", + fmt.Sprintf("source_type %s is not supported", sourceType), + ) + return + } + + data, ok := r.ReadFromAPI(ctx, &resp.Diagnostics, uuid, plan) + if !ok { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *applicationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state ApplicationModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "Reading application", map[string]interface{}{ + "uuid": state.Uuid.ValueString(), + }) + + if state.Uuid.ValueString() == "" { + resp.Diagnostics.AddError("Invalid State", "No UUID found in state") + return + } + + data, ok := r.ReadFromAPI(ctx, &resp.Diagnostics, state.Uuid.ValueString(), state) + if !ok { + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *applicationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan ApplicationModel + var state ApplicationModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + uuid := state.Uuid.ValueString() + if uuid == "" { + resp.Diagnostics.AddError("Invalid State", "No UUID found in state") + return + } + + tflog.Debug(ctx, "Updating application", map[string]interface{}{ + "uuid": uuid, + }) + + updateResp, err := r.client.UpdateApplicationByUuidWithResponse(ctx, uuid, plan.ToAPIUpdate()) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Error updating application: uuid=%s", uuid), + err.Error(), + ) + return + } + + if updateResp.StatusCode() != http.StatusOK { + resp.Diagnostics.AddError( + "Unexpected HTTP status code updating application", + fmt.Sprintf("Received %d updating application: uuid=%s. Details: %s", updateResp.StatusCode(), uuid, string(updateResp.Body)), + ) + return + } + + data, ok := r.ReadFromAPI(ctx, &resp.Diagnostics, uuid, plan) + if !ok { + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *applicationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ApplicationModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + uuid := state.Uuid.ValueString() + if uuid == "" { + resp.Diagnostics.AddError("Invalid State", "No UUID found in state") + return + } + + tflog.Debug(ctx, "Deleting application", map[string]interface{}{ + "uuid": uuid, + }) + + deleteTrue := true + deleteFalse := false + deleteResp, err := r.client.DeleteApplicationByUuidWithResponse(ctx, uuid, &api.DeleteApplicationByUuidParams{ + DeleteConfigurations: &deleteTrue, + DeleteVolumes: &deleteTrue, + DockerCleanup: &deleteTrue, + DeleteConnectedNetworks: &deleteFalse, + }) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete application, got error: %s", err)) + return + } + + if deleteResp.StatusCode() != http.StatusOK && deleteResp.StatusCode() != http.StatusNoContent { + resp.Diagnostics.AddError( + "Unexpected HTTP status code deleting application", + fmt.Sprintf("Received %d deleting application: %s. Details: %s", deleteResp.StatusCode(), uuid, string(deleteResp.Body)), + ) + return + } +} + +func (r *applicationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("uuid"), req, resp) +} + +func (r *applicationResource) ReadFromAPI( + ctx context.Context, + diags *diag.Diagnostics, + uuid string, + state ApplicationModel, +) (ApplicationModel, bool) { + res, err := r.client.GetApplicationByUuidWithResponse(ctx, uuid) + if err != nil { + diags.AddError( + fmt.Sprintf("Error reading application: uuid=%s", uuid), + err.Error(), + ) + return ApplicationModel{}, false + } + + if res.StatusCode() == http.StatusNotFound { + return ApplicationModel{}, false + } + + if res.StatusCode() != http.StatusOK { + diags.AddError( + "Unexpected HTTP status code reading application", + fmt.Sprintf("Received %d for application: uuid=%s. Details: %s", res.StatusCode(), uuid, string(res.Body)), + ) + return ApplicationModel{}, false + } + + result := ApplicationModel{}.FromAPI(res.JSON200, state) + return result, true +} + +func (r *applicationResource) validateCreatePlan(ctx context.Context, plan ApplicationModel) diag.Diagnostics { + var diags diag.Diagnostics + sourceType := ApplicationSourceType(plan.SourceType.ValueString()) + + switch sourceType { + case ApplicationSourceTypePublic, ApplicationSourceTypePrivateGithubApp, ApplicationSourceTypePrivateDeployKey: + if plan.GitRepository.IsNull() || plan.GitRepository.ValueString() == "" { + diags.AddAttributeError( + path.Root("git_repository"), + "Missing required field", + fmt.Sprintf("git_repository is required for source_type %s", sourceType), + ) + } + if plan.GitBranch.IsNull() || plan.GitBranch.ValueString() == "" { + diags.AddAttributeError( + path.Root("git_branch"), + "Missing required field", + fmt.Sprintf("git_branch is required for source_type %s", sourceType), + ) + } + if plan.BuildPack.IsNull() || plan.BuildPack.ValueString() == "" { + diags.AddAttributeError( + path.Root("build_pack"), + "Missing required field", + fmt.Sprintf("build_pack is required for source_type %s", sourceType), + ) + } + if plan.PortsExposes.IsNull() || plan.PortsExposes.ValueString() == "" { + diags.AddAttributeError( + path.Root("ports_exposes"), + "Missing required field", + fmt.Sprintf("ports_exposes is required for source_type %s", sourceType), + ) + } + if sourceType == ApplicationSourceTypePrivateGithubApp { + if plan.GithubAppUuid.IsNull() || plan.GithubAppUuid.ValueString() == "" { + diags.AddAttributeError( + path.Root("github_app_uuid"), + "Missing required field", + "github_app_uuid is required for source_type private-github-app", + ) + } + } + if sourceType == ApplicationSourceTypePrivateDeployKey { + if plan.PrivateKeyUuid.IsNull() || plan.PrivateKeyUuid.ValueString() == "" { + diags.AddAttributeError( + path.Root("private_key_uuid"), + "Missing required field", + "private_key_uuid is required for source_type private-deploy-key", + ) + } + } + case ApplicationSourceTypeDockerfile: + if plan.Dockerfile.IsNull() || plan.Dockerfile.ValueString() == "" { + diags.AddAttributeError( + path.Root("dockerfile"), + "Missing required field", + "dockerfile is required for source_type dockerfile", + ) + } + case ApplicationSourceTypeDockerimage: + if plan.DockerRegistryImageName.IsNull() || plan.DockerRegistryImageName.ValueString() == "" { + diags.AddAttributeError( + path.Root("docker_registry_image_name"), + "Missing required field", + "docker_registry_image_name is required for source_type dockerimage", + ) + } + if plan.PortsExposes.IsNull() || plan.PortsExposes.ValueString() == "" { + diags.AddAttributeError( + path.Root("ports_exposes"), + "Missing required field", + "ports_exposes is required for source_type dockerimage", + ) + } + case ApplicationSourceTypeDockercompose: + if plan.DockerComposeRaw.IsNull() || plan.DockerComposeRaw.ValueString() == "" { + diags.AddAttributeError( + path.Root("docker_compose_raw"), + "Missing required field", + "docker_compose_raw is required for source_type dockercompose", + ) + } + default: + diags.AddAttributeError( + path.Root("source_type"), + "Invalid source_type", + fmt.Sprintf("source_type %s is not supported. Valid values: public, private-github-app, private-deploy-key, dockerfile, dockerimage, dockercompose", sourceType), + ) + } + + return diags +} + diff --git a/internal/service/application_resource_acceptance_test.go b/internal/service/application_resource_acceptance_test.go new file mode 100644 index 0000000..7721c60 --- /dev/null +++ b/internal/service/application_resource_acceptance_test.go @@ -0,0 +1,157 @@ +package service_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + + "terraform-provider-coolify/internal/acctest" +) + +func TestAccApplicationResource_Public(t *testing.T) { + resName := "coolify_application.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { // Create and Read testing + Config: ` + resource "coolify_application" "test" { + source_type = "public" + project_uuid = "` + acctest.ProjectUUID + `" + server_uuid = "` + acctest.ServerUUID + `" + environment_name = "` + acctest.EnvironmentName + `" + git_repository = "https://github.com/coollabsio/coolify" + git_branch = "main" + build_pack = "nixpacks" + ports_exposes = "80" + name = "TerraformAccTest Public App" + description = "Terraform acceptance testing" + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resName, "source_type", "public"), + resource.TestCheckResourceAttr(resName, "name", "TerraformAccTest Public App"), + resource.TestCheckResourceAttr(resName, "description", "Terraform acceptance testing"), + resource.TestCheckResourceAttr(resName, "git_repository", "https://github.com/coollabsio/coolify"), + resource.TestCheckResourceAttr(resName, "git_branch", "main"), + resource.TestCheckResourceAttr(resName, "build_pack", "nixpacks"), + resource.TestCheckResourceAttr(resName, "ports_exposes", "80"), + // Verify dynamic values + resource.TestCheckResourceAttrSet(resName, "uuid"), + resource.TestCheckResourceAttrSet(resName, "id"), + ), + }, + { // ImportState testing + ResourceName: resName, + ImportState: true, + ImportStateVerify: true, + }, + { // Update and Read testing + Config: ` + resource "coolify_application" "test" { + source_type = "public" + project_uuid = "` + acctest.ProjectUUID + `" + server_uuid = "` + acctest.ServerUUID + `" + environment_name = "` + acctest.EnvironmentName + `" + git_repository = "https://github.com/coollabsio/coolify" + git_branch = "main" + build_pack = "nixpacks" + ports_exposes = "80" + name = "TerraformAccTest Public App Updated" + description = "Terraform acceptance testing updated" + } + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resName, plancheck.ResourceActionUpdate), + plancheck.ExpectKnownValue(resName, tfjsonpath.New("name"), knownvalue.StringExact("TerraformAccTest Public App Updated")), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resName, plancheck.ResourceActionNoop), + }, + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(resName, "uuid"), + resource.TestCheckResourceAttr(resName, "name", "TerraformAccTest Public App Updated"), + resource.TestCheckResourceAttr(resName, "description", "Terraform acceptance testing updated"), + ), + }, + }, + }) +} + +func TestAccApplicationResource_Dockerfile(t *testing.T) { + resName := "coolify_application.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { // Create and Read testing + Config: ` + resource "coolify_application" "test" { + source_type = "dockerfile" + project_uuid = "` + acctest.ProjectUUID + `" + server_uuid = "` + acctest.ServerUUID + `" + environment_name = "` + acctest.EnvironmentName + `" + dockerfile = < /dev/null; then + echo "❌ Go is not installed. Please install Go first." + exit 1 +fi + +OS=$(go env GOOS) +ARCH=$(go env GOARCH) +PLUGIN_DIR="$HOME/.terraform.d/plugins/registry.terraform.io/sierrajc/coolify/dev/${OS}_${ARCH}" + +echo -e "${BLUE}🔨 Building Terraform Coolify provider...${NC}" +echo " Directory: $PROVIDER_DIR" + +if [ -f "$PROVIDER_DIR/tools/tfplugingen-openapi.yml" ]; then + echo "📝 Generating code from OpenAPI..." + (cd "$PROVIDER_DIR" && make generate) || echo "⚠️ Generation failed, continuing..." +fi + +echo "📦 Installing provider to $PLUGIN_DIR..." +mkdir -p "$PLUGIN_DIR" + +BINARY_NAME="terraform-provider-coolify_dev_${OS}_${ARCH}" +BINARY_PATH="$PLUGIN_DIR/$BINARY_NAME" + +echo "🔨 Building provider..." +(cd "$PROVIDER_DIR" && go build -o "$BINARY_PATH" .) + +if [ ! -f "$BINARY_PATH" ]; then + echo "❌ Build failed" + exit 1 +fi + +chmod +x "$PLUGIN_DIR/$BINARY_NAME" + +echo "" +echo -e "${GREEN}✅ Provider installed successfully!${NC}" +echo "" +echo -e "${BLUE}Location:${NC} $PLUGIN_DIR/$BINARY_NAME" +echo "" +echo -e "${YELLOW}📋 Next steps:${NC}" +echo "1. In your Terraform project, create a file with:" +echo "" +echo " terraform {" +echo " required_providers {" +echo " coolify = {" +echo " source = \"registry.terraform.io/sierrajc/coolify\"" +echo " version = \"dev\"" +echo " }" +echo " }" +echo " }" +echo "" +echo "2. Configure the provider:" +echo "" +echo " provider \"coolify\" {" +echo " # token will be read from COOLIFY_TOKEN" +echo " }" +echo "" +echo "3. Initialize Terraform:" +echo "" +echo " terraform init" +echo "" +echo -e "${YELLOW}💡 Tip:${NC} Export your API token:" +echo " export COOLIFY_TOKEN=\"your-api-token\"" +echo " export COOLIFY_ENDPOINT=\"https://app.coolify.io/api/v1\" # Optional" + diff --git a/tools/tfplugingen-openapi.yml b/tools/tfplugingen-openapi.yml index dd70134..aaa0e78 100644 --- a/tools/tfplugingen-openapi.yml +++ b/tools/tfplugingen-openapi.yml @@ -93,6 +93,19 @@ resources: delete: path: /databases/{uuid} method: DELETE + application: + create: + path: /applications/public + method: POST + read: + path: /applications/{uuid} + method: GET + update: + path: /applications/{uuid} + method: PATCH + delete: + path: /applications/{uuid} + method: DELETE data_sources: private_keys: