Files
Max Vojtkov a6d8aa8b52 A Terraform provider for Dokploy
Plugin-framework provider covering projects, environments, applications,
Compose stacks, managed databases, domains, mounts, ports, redirects,
basic auth, registries, SSH keys, certificates and backup destinations,
over Dokploy's tRPC-over-REST API.

The shim package exposes the provider to other Go modules, which is how
pulumi-dokploy bridges it.
2026-08-09 12:17:26 +03:00

224 lines
8.7 KiB
Go

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"}
)