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.
255 lines
12 KiB
Go
255 lines
12 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
|
|
"github.com/maxvojtkov/terraform-provider-dokploy/internal/client"
|
|
)
|
|
|
|
// -------------------------------------------------------------------- Domain
|
|
|
|
type domainModel struct {
|
|
ID types.String `tfsdk:"id" dokploy:"domainId,id"`
|
|
Host types.String `tfsdk:"host" dokploy:"host"`
|
|
Path types.String `tfsdk:"path" dokploy:"path,nullable"`
|
|
Port types.Int64 `tfsdk:"port" dokploy:"port,nullable"`
|
|
HTTPS types.Bool `tfsdk:"https" dokploy:"https"`
|
|
CertificateType types.String `tfsdk:"certificate_type" dokploy:"certificateType"`
|
|
CustomCertResolver types.String `tfsdk:"custom_cert_resolver" dokploy:"customCertResolver,nullable"`
|
|
CustomEntrypoint types.String `tfsdk:"custom_entrypoint" dokploy:"customEntrypoint,nullable"`
|
|
DomainType types.String `tfsdk:"domain_type" dokploy:"domainType,nullable"`
|
|
ServiceName types.String `tfsdk:"service_name" dokploy:"serviceName,nullable"`
|
|
InternalPath types.String `tfsdk:"internal_path" dokploy:"internalPath,nullable"`
|
|
StripPath types.Bool `tfsdk:"strip_path" dokploy:"stripPath"`
|
|
Middlewares types.List `tfsdk:"middlewares" dokploy:"middlewares"`
|
|
ForwardAuthEnabled types.Bool `tfsdk:"forward_auth_enabled" dokploy:"forwardAuthEnabled"`
|
|
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,create"`
|
|
ComposeID types.String `tfsdk:"compose_id" dokploy:"composeId,create"`
|
|
PreviewDeploymentID types.String `tfsdk:"preview_deployment_id" dokploy:"previewDeploymentId,create"`
|
|
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
|
|
}
|
|
|
|
func domainResource() ResourceSpec {
|
|
return ResourceSpec{
|
|
Name: "domain",
|
|
CreateProc: "domain.create",
|
|
ReadProc: "domain.one",
|
|
UpdateProc: "domain.update",
|
|
DeleteProc: "domain.delete",
|
|
NewModel: func() any { return &domainModel{} },
|
|
Schema: schema.Schema{
|
|
MarkdownDescription: "A domain routed to an application or a Compose service through Dokploy's " +
|
|
"Traefik instance.\n\n" +
|
|
"Set exactly one of `application_id` or `compose_id`. When targeting a Compose stack, " +
|
|
"`service_name` selects which service in the stack receives the traffic.",
|
|
Attributes: map[string]schema.Attribute{
|
|
"id": computedID("Unique domain identifier."),
|
|
"host": requiredString("Fully-qualified hostname, for example `api.example.com`."),
|
|
"path": optionalComputedString("Path prefix this domain routes, defaults to `/`."),
|
|
"port": optionalComputedInt("Container port that receives the traffic, defaults to `3000`."),
|
|
"https": optionalComputedBool("Serve the domain over HTTPS and redirect HTTP traffic to it."),
|
|
"certificate_type": enumString(
|
|
"How TLS certificates are obtained. Use `letsencrypt` for automatic certificates.",
|
|
certificateTypes, false),
|
|
"custom_cert_resolver": optionalString("Traefik certificate resolver name, when " +
|
|
"`certificate_type` is `custom`."),
|
|
"custom_entrypoint": optionalString("Traefik entrypoint to bind, when not using the defaults."),
|
|
"domain_type": enumString("What kind of target this domain points at.", domainTypes, false),
|
|
"service_name": optionalString("Name of the service inside a Compose stack that receives the " +
|
|
"traffic. Required when `compose_id` is set."),
|
|
"internal_path": optionalComputedString("Path the request is rewritten to before it reaches the " +
|
|
"container, defaults to `/`."),
|
|
"strip_path": optionalComputedBool("Strip `path` from the request before forwarding it."),
|
|
"middlewares": optionalComputedStringList("Names of Traefik middlewares to apply."),
|
|
"forward_auth_enabled": optionalComputedBool("Protect this domain with Dokploy's forward auth."),
|
|
"application_id": optionalReplaceString("Application this domain routes to."),
|
|
"compose_id": optionalReplaceString("Compose stack this domain routes to."),
|
|
"preview_deployment_id": optionalReplaceString("Preview deployment this domain routes to."),
|
|
"created_at": computedString("RFC 3339 timestamp of when the domain was created."),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------------------------- Mount
|
|
|
|
type mountModel struct {
|
|
ID types.String `tfsdk:"id" dokploy:"mountId,id"`
|
|
Type types.String `tfsdk:"type" dokploy:"type"`
|
|
MountPath types.String `tfsdk:"mount_path" dokploy:"mountPath"`
|
|
HostPath types.String `tfsdk:"host_path" dokploy:"hostPath,nullable"`
|
|
VolumeName types.String `tfsdk:"volume_name" dokploy:"volumeName,nullable"`
|
|
FilePath types.String `tfsdk:"file_path" dokploy:"filePath,nullable"`
|
|
Content types.String `tfsdk:"content" dokploy:"content,nullable"`
|
|
ServiceType types.String `tfsdk:"service_type" dokploy:"serviceType"`
|
|
|
|
// serviceId is only accepted on create; reads return the concrete
|
|
// applicationId/composeId/... column instead, so it is never refreshed.
|
|
ServiceID types.String `tfsdk:"service_id" dokploy:"serviceId,create"`
|
|
}
|
|
|
|
func mountResource() ResourceSpec {
|
|
return ResourceSpec{
|
|
Name: "mount",
|
|
CreateProc: "mounts.create",
|
|
ReadProc: "mounts.one",
|
|
UpdateProc: "mounts.update",
|
|
DeleteProc: "mounts.remove",
|
|
NewModel: func() any { return &mountModel{} },
|
|
Schema: schema.Schema{
|
|
MarkdownDescription: "A volume, bind mount, or config file attached to a Dokploy service.\n\n" +
|
|
"* `type = \"volume\"` — a named Docker volume; set `volume_name`.\n" +
|
|
"* `type = \"bind\"` — a path on the host; set `host_path`.\n" +
|
|
"* `type = \"file\"` — a file rendered from `content`; set `file_path`.",
|
|
Attributes: map[string]schema.Attribute{
|
|
"id": computedID("Unique mount identifier."),
|
|
"type": enumString("The kind of mount to create.", mountTypes, true),
|
|
"mount_path": requiredString("Path inside the container where the mount appears."),
|
|
"host_path": optionalString("Path on the host, when `type` is `bind`."),
|
|
"volume_name": optionalString("Name of the Docker volume, when `type` is `volume`."),
|
|
"file_path": optionalString("Path of the generated file, when `type` is `file`."),
|
|
"content": optionalString("Contents of the generated file, when `type` is `file`."),
|
|
"service_type": enumString("The kind of service this mount attaches to.", serviceTypes, true),
|
|
"service_id": requiredReplaceString("ID of the service this mount attaches to. Must match " +
|
|
"`service_type` — an application ID, a compose ID, a postgres ID, and so on."),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------- Port
|
|
|
|
type portModel struct {
|
|
ID types.String `tfsdk:"id" dokploy:"portId,id"`
|
|
PublishedPort types.Int64 `tfsdk:"published_port" dokploy:"publishedPort"`
|
|
TargetPort types.Int64 `tfsdk:"target_port" dokploy:"targetPort"`
|
|
Protocol types.String `tfsdk:"protocol" dokploy:"protocol"`
|
|
PublishMode types.String `tfsdk:"publish_mode" dokploy:"publishMode"`
|
|
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,create"`
|
|
}
|
|
|
|
func portResource() ResourceSpec {
|
|
return ResourceSpec{
|
|
Name: "port",
|
|
CreateProc: "port.create",
|
|
ReadProc: "port.one",
|
|
UpdateProc: "port.update",
|
|
DeleteProc: "port.delete",
|
|
NewModel: func() any { return &portModel{} },
|
|
Schema: schema.Schema{
|
|
MarkdownDescription: "A published port that exposes an application directly on the host, " +
|
|
"bypassing Traefik.",
|
|
Attributes: map[string]schema.Attribute{
|
|
"id": computedID("Unique port identifier."),
|
|
"published_port": requiredInt("Port exposed on the host."),
|
|
"target_port": requiredInt("Port the container listens on."),
|
|
"protocol": enumString("Transport protocol.", protocolTypes, true),
|
|
"publish_mode": enumString("Docker Swarm publish mode. `host` binds directly to the node; "+
|
|
"`ingress` uses the swarm routing mesh.", publishModes, false),
|
|
"application_id": requiredReplaceString("Application this port belongs to."),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ Redirect
|
|
|
|
type redirectModel struct {
|
|
ID types.String `tfsdk:"id" dokploy:"redirectId,id"`
|
|
Regex types.String `tfsdk:"regex" dokploy:"regex"`
|
|
Replacement types.String `tfsdk:"replacement" dokploy:"replacement"`
|
|
Permanent types.Bool `tfsdk:"permanent" dokploy:"permanent"`
|
|
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,create"`
|
|
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
|
|
}
|
|
|
|
func redirectResource() ResourceSpec {
|
|
return ResourceSpec{
|
|
Name: "redirect",
|
|
CreateProc: "redirects.create",
|
|
ReadProc: "redirects.one",
|
|
UpdateProc: "redirects.update",
|
|
DeleteProc: "redirects.delete",
|
|
NewModel: func() any { return &redirectModel{} },
|
|
// `redirects.create` returns `true`, so the new ID is discovered by
|
|
// diffing the application's redirect list.
|
|
ListIDs: func(ctx context.Context, api *client.Client, model any) (map[string]struct{}, error) {
|
|
redirect, ok := model.(*redirectModel)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected *redirectModel, got %T", model)
|
|
}
|
|
raw, err := api.Query(ctx, "application.one", map[string]any{
|
|
"applicationId": redirect.ApplicationID.ValueString(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return collectNestedIDs(raw, "redirects", "redirectId")
|
|
},
|
|
Schema: schema.Schema{
|
|
MarkdownDescription: "A Traefik redirect rule attached to an application.",
|
|
Attributes: map[string]schema.Attribute{
|
|
"id": computedID("Unique redirect identifier."),
|
|
"regex": requiredString("Regular expression matched against the incoming URL."),
|
|
"replacement": requiredString("Replacement URL, which may reference capture groups such as `${1}`."),
|
|
"permanent": optionalComputedBool("Issue a permanent (301) redirect instead of a temporary " +
|
|
"(302) one."),
|
|
"application_id": requiredReplaceString("Application this redirect belongs to."),
|
|
"created_at": computedString("RFC 3339 timestamp of when the redirect was created."),
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------ Security
|
|
|
|
type securityModel struct {
|
|
ID types.String `tfsdk:"id" dokploy:"securityId,id"`
|
|
Username types.String `tfsdk:"username" dokploy:"username"`
|
|
Password types.String `tfsdk:"password" dokploy:"password,noread"`
|
|
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,create"`
|
|
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
|
|
}
|
|
|
|
func securityResource() ResourceSpec {
|
|
return ResourceSpec{
|
|
Name: "security",
|
|
CreateProc: "security.create",
|
|
ReadProc: "security.one",
|
|
UpdateProc: "security.update",
|
|
DeleteProc: "security.delete",
|
|
NewModel: func() any { return &securityModel{} },
|
|
// `security.create` returns `true`, so the new ID is discovered by
|
|
// diffing the application's basic-auth credential list.
|
|
ListIDs: func(ctx context.Context, api *client.Client, model any) (map[string]struct{}, error) {
|
|
security, ok := model.(*securityModel)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected *securityModel, got %T", model)
|
|
}
|
|
raw, err := api.Query(ctx, "application.one", map[string]any{
|
|
"applicationId": security.ApplicationID.ValueString(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return collectNestedIDs(raw, "security", "securityId")
|
|
},
|
|
Schema: schema.Schema{
|
|
MarkdownDescription: "HTTP basic authentication credentials protecting an application's domains.\n\n" +
|
|
"~> Dokploy stores the password hashed and does not return it. The value in Terraform state is " +
|
|
"the one you configured.",
|
|
Attributes: map[string]schema.Attribute{
|
|
"id": computedID("Unique credential identifier."),
|
|
"username": requiredString("Basic auth username."),
|
|
"password": sensitiveString("Basic auth password.", true),
|
|
"application_id": requiredReplaceString("Application these credentials protect."),
|
|
"created_at": computedString("RFC 3339 timestamp of when the credentials were created."),
|
|
},
|
|
},
|
|
}
|
|
}
|