package provider import ( "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) // Shorthand schema constructors. Dokploy assigns server-side defaults to most // optional fields, so nearly everything optional is also Computed: that lets // the API supply a value without Terraform reporting an inconsistent result. func computedID(description string) schema.StringAttribute { return schema.StringAttribute{ Computed: true, MarkdownDescription: description, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, } } func requiredString(description string) schema.StringAttribute { return schema.StringAttribute{Required: true, MarkdownDescription: description} } // requiredReplaceString is a required attribute that cannot be changed in // place; Dokploy has no API to move the resource, so Terraform recreates it. func requiredReplaceString(description string) schema.StringAttribute { return schema.StringAttribute{ Required: true, MarkdownDescription: description, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, } } func optionalReplaceString(description string) schema.StringAttribute { return schema.StringAttribute{ Optional: true, MarkdownDescription: description, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, } } // requiresReplaceString is the plan-modifier list for an attribute that cannot // be changed in place. func requiresReplaceString() []planmodifier.String { return []planmodifier.String{stringplanmodifier.RequiresReplace()} } func optionalString(description string) schema.StringAttribute { return schema.StringAttribute{Optional: true, MarkdownDescription: description} } // computedString is a server-assigned value that Terraform never sends. // // UseStateForUnknown is essential here: without it every computed attribute is // planned as unknown during an update, and any attribute referencing one -- // `environment_id = dokploy_project.x.default_environment_id`, say -- would // then be unknown too, forcing a spurious replacement of the dependent // resource. Read still refreshes these values, so genuine drift is detected. func computedString(description string) schema.StringAttribute { return schema.StringAttribute{ Computed: true, MarkdownDescription: description, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, } } // optionalComputedString is the workhorse: settable by the practitioner, and // defaulted by Dokploy when omitted. // // UseStateForUnknown keeps plans readable. Without it the framework replans // every unconfigured Optional+Computed attribute as unknown as soon as // anything else on the resource changes, so a one-line edit renders as a dozen // "(known after apply)" lines. Dokploy assigns these defaults once at create // and then stores them, so reusing the prior value is accurate -- and Read // still refreshes them, so real drift is still caught. func optionalComputedString(description string) schema.StringAttribute { return schema.StringAttribute{ Optional: true, Computed: true, MarkdownDescription: description, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, } } // optionalComputedReplaceString is defaulted by Dokploy when omitted, but // changing an explicitly configured value forces recreation. func optionalComputedReplaceString(description string) schema.StringAttribute { return schema.StringAttribute{ Optional: true, Computed: true, MarkdownDescription: description, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), stringplanmodifier.UseStateForUnknown(), }, } } func sensitiveString(description string, required bool) schema.StringAttribute { return schema.StringAttribute{ Required: required, Optional: !required, Computed: !required, Sensitive: true, MarkdownDescription: description, } } func enumString(description string, values []string, required bool) schema.StringAttribute { return schema.StringAttribute{ Required: required, Optional: !required, Computed: !required, MarkdownDescription: description + " Valid values: `" + joinBackticked(values) + "`.", Validators: []validator.String{stringvalidator.OneOf(values...)}, } } // enumStringWithDefault is an enum whose value Dokploy insists on receiving // even though only one value is currently valid. func enumStringWithDefault(description string, values []string, def string) schema.StringAttribute { return schema.StringAttribute{ Optional: true, Computed: true, MarkdownDescription: description + " Valid values: `" + joinBackticked(values) + "`. Defaults to `" + def + "`.", Validators: []validator.String{stringvalidator.OneOf(values...)}, Default: stringdefault.StaticString(def), } } // computedBool mirrors computedString for boolean server-assigned values. func computedBool(description string) schema.BoolAttribute { return schema.BoolAttribute{ Computed: true, MarkdownDescription: description, PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, } } func optionalComputedBool(description string) schema.BoolAttribute { return schema.BoolAttribute{ Optional: true, Computed: true, MarkdownDescription: description, PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, } } // optionalBool has no server-side counterpart; it only steers provider // behaviour. It is deliberately not Computed, so an unset value stays null and // an imported resource shows no phantom diff. func optionalBool(description string) schema.BoolAttribute { return schema.BoolAttribute{Optional: true, MarkdownDescription: description} } func optionalComputedInt(description string) schema.Int64Attribute { return schema.Int64Attribute{ Optional: true, Computed: true, MarkdownDescription: description, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, } } func requiredInt(description string) schema.Int64Attribute { return schema.Int64Attribute{Required: true, MarkdownDescription: description} } func optionalComputedStringList(description string) schema.ListAttribute { return schema.ListAttribute{ Optional: true, Computed: true, ElementType: types.StringType, MarkdownDescription: description, PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, } } // optionalJSON exposes one of Dokploy's free-form JSON columns (the Docker // Swarm service settings) as a normalized JSON string, so semantically equal // documents do not produce a diff. func optionalJSON(description string) schema.StringAttribute { return schema.StringAttribute{ Optional: true, CustomType: jsontypes.NormalizedType{}, MarkdownDescription: description, } } func joinBackticked(values []string) string { out := "" for i, v := range values { if i > 0 { out += "`, `" } out += v } return out } // Enum value sets mirrored from Dokploy's Postgres enums. var ( certificateTypes = []string{"letsencrypt", "none", "custom"} sourceTypes = []string{"docker", "git", "github", "gitlab", "bitbucket", "gitea", "drop"} buildTypes = []string{"dockerfile", "heroku_buildpacks", "paketo_buildpacks", "nixpacks", "static", "railpack"} triggerTypes = []string{"push", "tag"} composeTypes = []string{"docker-compose", "stack"} composeSources = []string{"git", "github", "gitlab", "bitbucket", "gitea", "raw"} domainTypes = []string{"compose", "application", "preview"} mountTypes = []string{"bind", "volume", "file"} serviceTypes = []string{"application", "postgres", "mysql", "mariadb", "mongo", "redis", "compose"} protocolTypes = []string{"tcp", "udp"} publishModes = []string{"ingress", "host"} )