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.
224 lines
8.7 KiB
Go
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"}
|
|
)
|