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:
254
internal/provider/resource_networking.go
Normal file
254
internal/provider/resource_networking.go
Normal file
@@ -0,0 +1,254 @@
|
||||
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."),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user