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