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.
This commit is contained in:
2026-08-09 12:17:26 +03:00
commit a6d8aa8b52
160 changed files with 24260 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/maxvojtkov/terraform-provider-dokploy/internal/client"
)
// ------------------------------------------------------------------ Registry
type registryModel struct {
ID types.String `tfsdk:"id" dokploy:"registryId,id"`
RegistryName types.String `tfsdk:"registry_name" dokploy:"registryName"`
Username types.String `tfsdk:"username" dokploy:"username"`
Password types.String `tfsdk:"password" dokploy:"password"`
RegistryURL types.String `tfsdk:"registry_url" dokploy:"registryUrl"`
RegistryType types.String `tfsdk:"registry_type" dokploy:"registryType"`
ImagePrefix types.String `tfsdk:"image_prefix" dokploy:"imagePrefix,nullable"`
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,create"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func registryResource() ResourceSpec {
return ResourceSpec{
Name: "registry",
CreateProc: "registry.create",
ReadProc: "registry.one",
UpdateProc: "registry.update",
DeleteProc: "registry.remove",
NewModel: func() any { return &registryModel{} },
Schema: schema.Schema{
MarkdownDescription: "A container registry that Dokploy pushes built images to and pulls them from.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique registry identifier."),
"registry_name": requiredString("Display name of the registry."),
"username": requiredString("Username used to authenticate to the registry."),
"password": sensitiveString("Password or access token used to authenticate.", true),
"registry_url": requiredString("Registry hostname, for example `ghcr.io`."),
"registry_type": enumStringWithDefault("Registry kind. Dokploy currently accepts only `cloud`.",
[]string{"cloud"}, "cloud"),
"image_prefix": optionalString("Prefix prepended to pushed image names, for example an " +
"organization or namespace."),
"server_id": optionalReplaceString("Server this registry is scoped to."),
"created_at": computedString("RFC 3339 timestamp of when the registry was created."),
},
},
}
}
// ------------------------------------------------------------------- SSH key
type sshKeyModel struct {
ID types.String `tfsdk:"id" dokploy:"sshKeyId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
Description types.String `tfsdk:"description" dokploy:"description,nullable"`
PublicKey types.String `tfsdk:"public_key" dokploy:"publicKey,create"`
PrivateKey types.String `tfsdk:"private_key" dokploy:"privateKey,create"`
LastUsedAt types.String `tfsdk:"last_used_at" dokploy:"lastUsedAt,ro"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func sshKeyResource() ResourceSpec {
return ResourceSpec{
Name: "ssh_key",
NeedsOrganizationID: true,
CreateProc: "sshKey.create",
ReadProc: "sshKey.one",
UpdateProc: "sshKey.update",
DeleteProc: "sshKey.remove",
NewModel: func() any { return &sshKeyModel{} },
// `sshKey.create` returns nothing, so the new ID is discovered by
// diffing the key list around the call.
ListIDs: func(ctx context.Context, api *client.Client, _ any) (map[string]struct{}, error) {
raw, err := api.Query(ctx, "sshKey.all", nil)
if err != nil {
return nil, err
}
return collectIDs(raw, "sshKeyId")
},
Schema: schema.Schema{
MarkdownDescription: "An SSH key pair Dokploy uses to clone private Git repositories and to reach " +
"remote servers.\n\n" +
"~> The private key is stored in Terraform state. Use a state backend with encryption at rest.\n\n" +
"~> Dokploy's update endpoint only accepts `name` and `description`. Changing either key forces " +
"a new resource.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique SSH key identifier."),
"name": requiredString("Display name of the key pair."),
"description": optionalString("Free-form description."),
"public_key": requiredReplaceString("OpenSSH-formatted public key."),
"private_key": schema.StringAttribute{
Required: true,
Sensitive: true,
MarkdownDescription: "PEM-encoded private key.",
PlanModifiers: requiresReplaceString(),
},
"last_used_at": computedString("RFC 3339 timestamp of when the key was last used, if ever."),
"created_at": computedString("RFC 3339 timestamp of when the key was created."),
},
},
}
}
// --------------------------------------------------------------- Certificate
type certificateModel struct {
ID types.String `tfsdk:"id" dokploy:"certificateId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
CertificateData types.String `tfsdk:"certificate_data" dokploy:"certificateData"`
PrivateKey types.String `tfsdk:"private_key" dokploy:"privateKey"`
AutoRenew types.Bool `tfsdk:"auto_renew" dokploy:"autoRenew,create"`
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,create"`
CertificatePath types.String `tfsdk:"certificate_path" dokploy:"certificatePath,ro"`
}
func certificateResource() ResourceSpec {
return ResourceSpec{
Name: "certificate",
NeedsOrganizationID: true,
CreateProc: "certificates.create",
ReadProc: "certificates.one",
UpdateProc: "certificates.update",
DeleteProc: "certificates.remove",
NewModel: func() any { return &certificateModel{} },
Schema: schema.Schema{
MarkdownDescription: "A TLS certificate uploaded to Dokploy, for domains that use " +
"`certificate_type = \"custom\"`.\n\n" +
"~> The private key is stored in Terraform state. Use a state backend with encryption at rest.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique certificate identifier."),
"name": requiredString("Display name of the certificate."),
"certificate_data": requiredString("PEM-encoded certificate chain."),
"private_key": sensitiveString("PEM-encoded private key.", true),
"auto_renew": optionalComputedBool("Whether Dokploy should renew this certificate automatically."),
"server_id": optionalReplaceString("Server this certificate is installed on."),
"certificate_path": computedString("Path where Dokploy writes the certificate on disk."),
},
},
}
}
// --------------------------------------------------------------- Destination
type destinationModel struct {
ID types.String `tfsdk:"id" dokploy:"destinationId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
// `provider` is a reserved root attribute name in Terraform, so the
// attribute is exposed as `provider_name`.
Provider types.String `tfsdk:"provider_name" dokploy:"provider,nullable"`
AccessKey types.String `tfsdk:"access_key" dokploy:"accessKey"`
SecretAccessKey types.String `tfsdk:"secret_access_key" dokploy:"secretAccessKey"`
Bucket types.String `tfsdk:"bucket" dokploy:"bucket"`
Region types.String `tfsdk:"region" dokploy:"region"`
Endpoint types.String `tfsdk:"endpoint" dokploy:"endpoint"`
AdditionalFlags types.List `tfsdk:"additional_flags" dokploy:"additionalFlags,nullable"`
ServerID types.String `tfsdk:"server_id" dokploy:"serverId"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func destinationResource() ResourceSpec {
return ResourceSpec{
Name: "destination",
CreateProc: "destination.create",
ReadProc: "destination.one",
UpdateProc: "destination.update",
DeleteProc: "destination.remove",
NewModel: func() any { return &destinationModel{} },
Schema: schema.Schema{
MarkdownDescription: "An S3-compatible bucket that Dokploy writes database and volume backups to.\n\n" +
"~> The secret access key is stored in Terraform state. Use a state backend with encryption at rest.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique destination identifier."),
"name": requiredString("Display name of the destination."),
"provider_name": optionalString("Provider label, for example `s3` or `cloudflare`. " +
"Named `provider_name` because `provider` is reserved by Terraform."),
"access_key": requiredString("S3 access key ID."),
"secret_access_key": sensitiveString("S3 secret access key.", true),
"bucket": requiredString("Bucket name."),
"region": requiredString("Bucket region, for example `us-east-1`."),
"endpoint": requiredString("S3 endpoint URL."),
"additional_flags": optionalComputedStringList("Extra flags passed to the underlying `rclone` invocation."),
"server_id": optionalString("Server this destination is scoped to."),
"created_at": computedString("Timestamp of when the destination was created."),
},
},
}
}