Files
terraform-provider-dokploy/internal/provider/resource_networking.go
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

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