Cover what Dokploy v0.30 added
build / build (push) Successful in 3m56s
release / release (push) Successful in 15m2s

Six new resources, all backed by endpoints that did not exist before v0.30.0
and verified end-to-end against a live v0.30.2 instance:

  dokploy_network         Docker networks, now first-class. Services attach
                          through network_ids, which is what deprecates
                          Compose's isolated_deployment upstream.
  dokploy_dns_provider    Cloudflare or Route53, so adding a domain creates
                          its DNS record.
  dokploy_vault_provider  Env values resolved from HashiCorp Vault, Infisical,
                          AWS, Doppler, Azure or Scaleway at deploy time, so
                          the secret never lands in Dokploy or in state.
  dokploy_schedule        Cron jobs in a container, a stack, or on a server.
  dokploy_volume_backup   Scheduled backups of a named volume — the companion
                          to a mount that persists.
  dokploy_libsql          The sixth managed database engine.

libsql.create is the strictest endpoint in the API: eleven keys required to
be present, several only meaningfully null, no generated service name, and it
returns `true` rather than the row. CreateDefaults and ListIDs absorb all
three so the resource behaves like every other database.

Also filled the gaps a field-by-field diff against the live schema turned up:
domain gains `enabled` (the v0.30.0 park-a-domain toggle), compose gains
create_env_file, icon and service_networks, application gains icon and
preview_require_collaborator_permissions, and mounts accept libsql.

DNS and vault credentials are masked by Dokploy on read, so `config` is
tagged noread and keeps the configured value, as the basic-auth password
already does.
This commit is contained in:
max-voitcov
2026-08-26 00:40:27 +03:00
parent 3ddce62647
commit 2d1caf6e73
23 changed files with 1297 additions and 12 deletions
+13 -1
View File
@@ -44,7 +44,7 @@ func (p *dokployProvider) Metadata(_ context.Context, _ provider.MetadataRequest
func (p *dokployProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Manage [Dokploy](https://dokploy.com) projects, environments, applications, " +
"compose stacks, databases and networking with Terraform.",
"compose stacks, databases, Docker networks and networking with Terraform.",
Attributes: map[string]schema.Attribute{
"host": schema.StringAttribute{
Optional: true,
@@ -139,6 +139,7 @@ func (p *dokployProvider) Resources(_ context.Context) []func() resource.Resourc
newGenericResource(mariadbResource()),
newGenericResource(mongoResource()),
newGenericResource(redisResource()),
newGenericResource(libsqlResource()),
newGenericResource(domainResource()),
newGenericResource(mountResource()),
@@ -146,6 +147,17 @@ func (p *dokployProvider) Resources(_ context.Context) []func() resource.Resourc
newGenericResource(redirectResource()),
newGenericResource(securityResource()),
// Docker network management, added in Dokploy v0.30.0.
newGenericResource(networkResource()),
// Scheduling, added in Dokploy v0.30.0.
newGenericResource(scheduleResource()),
newGenericResource(volumeBackupResource()),
// External integrations, added in Dokploy v0.30.0.
newGenericResource(dnsProviderResource()),
newGenericResource(vaultProviderResource()),
newGenericResource(registryResource()),
newGenericResource(sshKeyResource()),
newGenericResource(certificateResource()),
+6 -1
View File
@@ -75,6 +75,7 @@ type applicationModel struct {
MemoryLimit types.String `tfsdk:"memory_limit" dokploy:"memoryLimit,nullable"`
CPUReserve types.String `tfsdk:"cpu_reservation" dokploy:"cpuReservation,nullable"`
CPULimit types.String `tfsdk:"cpu_limit" dokploy:"cpuLimit,nullable"`
Icon types.String `tfsdk:"icon" dokploy:"icon,nullable"`
Title types.String `tfsdk:"title" dokploy:"title,nullable"`
Subtitle types.String `tfsdk:"subtitle" dokploy:"subtitle,nullable"`
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled,nullable"`
@@ -113,6 +114,7 @@ type applicationModel struct {
PreviewCertType types.String `tfsdk:"preview_certificate_type" dokploy:"previewCertificateType,nullable"`
PreviewCertResolver types.String `tfsdk:"preview_custom_cert_resolver" dokploy:"previewCustomCertResolver,nullable"`
PreviewLimit types.Int64 `tfsdk:"preview_limit" dokploy:"previewLimit,nullable"`
PreviewCollabPerms types.Bool `tfsdk:"preview_require_collaborator_permissions" dokploy:"previewRequireCollaboratorPermissions,nullable"`
PreviewLabels types.List `tfsdk:"preview_labels" dokploy:"previewLabels,nullable"`
// Computed
@@ -211,6 +213,7 @@ func applicationResource() ResourceSpec {
"memory_limit": optionalString("Hard memory limit, for example `512m`."),
"cpu_reservation": optionalString("Soft CPU reservation, for example `0.5`."),
"cpu_limit": optionalString("Hard CPU limit, for example `1`."),
"icon": optionalString("Icon shown next to the service in the Dokploy UI."),
"title": optionalString("Display title shown in the Dokploy UI."),
"subtitle": optionalString("Display subtitle shown in the Dokploy UI."),
"enabled": optionalComputedBool("Whether the application is enabled."),
@@ -247,7 +250,9 @@ func applicationResource() ResourceSpec {
"preview_certificate_type": enumString("Certificate strategy for preview deployments.", certificateTypes, false),
"preview_custom_cert_resolver": optionalString("Traefik certificate resolver for preview deployments."),
"preview_limit": optionalComputedInt("Maximum number of concurrent preview deployments."),
"preview_labels": optionalComputedStringList("Pull request labels that opt into preview deployments."),
"preview_require_collaborator_permissions": optionalComputedBool("Only build previews for pull " +
"requests opened by users with repository collaborator permissions."),
"preview_labels": optionalComputedStringList("Pull request labels that opt into preview deployments."),
"application_status": computedString("Current status reported by Dokploy: `idle`, `running`, `done` or `error`."),
"created_at": computedString("RFC 3339 timestamp of when the application was created."),
+13
View File
@@ -1,6 +1,7 @@
package provider
import (
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -19,6 +20,13 @@ type composeModel struct {
SourceType types.String `tfsdk:"source_type" dokploy:"sourceType"`
Command types.String `tfsdk:"command" dokploy:"command"`
Env types.String `tfsdk:"env" dokploy:"env,nullable"`
Icon types.String `tfsdk:"icon" dokploy:"icon,nullable"`
CreateEnvFile types.Bool `tfsdk:"create_env_file" dokploy:"createEnvFile"`
// serviceNetworks maps a service name in the stack to the networks it
// joins, so a stack can attach per-service rather than as a whole.
ServiceNetworks jsontypes.Normalized `tfsdk:"service_networks" dokploy:"serviceNetworks,nullable"`
Repository types.String `tfsdk:"repository" dokploy:"repository,nullable"`
Owner types.String `tfsdk:"owner" dokploy:"owner,nullable"`
@@ -97,6 +105,11 @@ func composeResource() ResourceSpec {
"source_type": enumString("Where the Compose file comes from.", composeSources, false),
"command": optionalComputedString("Custom `docker compose` command to run."),
"env": optionalString("Environment variables in `KEY=value` format, one per line."),
"icon": optionalString("Icon shown next to the stack in the Dokploy UI."),
"create_env_file": optionalComputedBool("Write the environment variables to a `.env` file next to " +
"the Compose file."),
"service_networks": optionalJSON("Per-service Docker network attachments, as a JSON object mapping " +
"each service name in the stack to an array of network IDs."),
"repository": optionalString("GitHub repository name."),
"owner": optionalString("GitHub repository owner."),
+206
View File
@@ -0,0 +1,206 @@
package provider
import (
"context"
"crypto/rand"
"fmt"
"strings"
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/maxvojtkov/terraform-provider-dokploy/internal/client"
)
// libSQL is the sixth managed database engine, added in Dokploy v0.30.0.
//
// Its create endpoint is the most awkward in the API: it insists on eleven
// keys being present (several only meaningfully null), it will not generate an
// appName the way the other engines do, and it returns `true` rather than the
// created row. All three are worked around below.
type libsqlModel struct {
ID types.String `tfsdk:"id" dokploy:"libsqlId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
AppName types.String `tfsdk:"app_name" dokploy:"appName"`
Description types.String `tfsdk:"description" dokploy:"description,nullable"`
EnvironmentID types.String `tfsdk:"environment_id" dokploy:"environmentId"`
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,nullable"`
DatabaseUser types.String `tfsdk:"database_user" dokploy:"databaseUser"`
DatabasePassword types.String `tfsdk:"database_password" dokploy:"databasePassword"`
DockerImage types.String `tfsdk:"docker_image" dokploy:"dockerImage"`
SqldNode types.String `tfsdk:"sqld_node" dokploy:"sqldNode"`
SqldPrimaryURL types.String `tfsdk:"sqld_primary_url" dokploy:"sqldPrimaryUrl,nullable"`
EnableNamespaces types.Bool `tfsdk:"enable_namespaces" dokploy:"enableNamespaces"`
Command types.String `tfsdk:"command" dokploy:"command,nullable"`
Env types.String `tfsdk:"env" dokploy:"env,nullable"`
MemoryReserve types.String `tfsdk:"memory_reservation" dokploy:"memoryReservation,nullable"`
MemoryLimit types.String `tfsdk:"memory_limit" dokploy:"memoryLimit,nullable"`
CPUReserve types.String `tfsdk:"cpu_reservation" dokploy:"cpuReservation,nullable"`
CPULimit types.String `tfsdk:"cpu_limit" dokploy:"cpuLimit,nullable"`
Replicas types.Int64 `tfsdk:"replicas" dokploy:"replicas"`
ExternalPort types.Int64 `tfsdk:"external_port" dokploy:"externalPort,nullable"`
ExternalAdminPort types.Int64 `tfsdk:"external_admin_port" dokploy:"externalAdminPort,nullable"`
ExternalGRPCPort types.Int64 `tfsdk:"external_grpc_port" dokploy:"externalGRPCPort,nullable"`
NetworkIDs types.List `tfsdk:"network_ids" dokploy:"networkIds"`
DetachDokployNetwork types.Bool `tfsdk:"detach_dokploy_network" dokploy:"detachDokployNetwork"`
HealthCheckSwarm jsontypes.Normalized `tfsdk:"health_check_swarm" dokploy:"healthCheckSwarm,nullable"`
RestartPolicySwarm jsontypes.Normalized `tfsdk:"restart_policy_swarm" dokploy:"restartPolicySwarm,nullable"`
PlacementSwarm jsontypes.Normalized `tfsdk:"placement_swarm" dokploy:"placementSwarm,nullable"`
UpdateConfigSwarm jsontypes.Normalized `tfsdk:"update_config_swarm" dokploy:"updateConfigSwarm,nullable"`
RollbackConfigSwarm jsontypes.Normalized `tfsdk:"rollback_config_swarm" dokploy:"rollbackConfigSwarm,nullable"`
ModeSwarm jsontypes.Normalized `tfsdk:"mode_swarm" dokploy:"modeSwarm,nullable"`
LabelsSwarm jsontypes.Normalized `tfsdk:"labels_swarm" dokploy:"labelsSwarm,nullable"`
NetworkSwarm jsontypes.Normalized `tfsdk:"network_swarm" dokploy:"networkSwarm,nullable"`
EndpointSpecSwarm jsontypes.Normalized `tfsdk:"endpoint_spec_swarm" dokploy:"endpointSpecSwarm,nullable"`
StopGracePeriodSwarm types.Int64 `tfsdk:"stop_grace_period_swarm" dokploy:"stopGracePeriodSwarm,nullable"`
ApplicationStatus types.String `tfsdk:"application_status" dokploy:"applicationStatus,ro"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func libsqlResource() ResourceSpec {
return ResourceSpec{
Name: "libsql",
UpdateAfterCreate: true,
CreateProc: "libsql.create",
ReadProc: "libsql.one",
UpdateProc: "libsql.update",
DeleteProc: "libsql.remove",
NewModel: func() any { return &libsqlModel{} },
// `libsql.create` rejects a body that merely omits a key it considers
// required, even when null is the only sensible value, and it refuses
// to generate an appName. Fill both in.
CreateDefaults: func(model any) map[string]any {
libsql, ok := model.(*libsqlModel)
if !ok {
return nil
}
defaults := map[string]any{
"description": nil,
"serverId": nil,
"sqldPrimaryUrl": nil,
}
if libsql.AppName.IsNull() || libsql.AppName.IsUnknown() {
defaults["appName"] = generateAppName(libsql.Name.ValueString())
}
return defaults
},
// `libsql.create` returns `true`, so the new ID is found by diffing the
// environment's libSQL list.
ListIDs: func(ctx context.Context, api *client.Client, model any) (map[string]struct{}, error) {
libsql, ok := model.(*libsqlModel)
if !ok {
return nil, fmt.Errorf("expected *libsqlModel, got %T", model)
}
raw, err := api.Query(ctx, "environment.one", map[string]any{
"environmentId": libsql.EnvironmentID.ValueString(),
})
if err != nil {
return nil, err
}
return collectNestedIDs(raw, "libsql", "libsqlId")
},
Schema: schema.Schema{
MarkdownDescription: databaseNote("libSQL") + "\n\n" +
"libSQL runs as a `sqld` server. A `primary` node owns the data; a `replica` node follows a " +
"primary named by `sqld_primary_url`.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique libSQL identifier."),
"name": requiredString("Display name of the database."),
"app_name": optionalComputedReplaceString("Unique Docker service name. Generated from `name` when " +
"omitted, because Dokploy's libSQL endpoint does not generate one. Changing it forces a new database."),
"description": optionalString("Free-form description."),
"environment_id": requiredReplaceString("Environment this database belongs to."),
"server_id": optionalReplaceString("Remote server to deploy on. Omit to use the Dokploy host itself."),
"database_user": requiredString("Database user to create."),
"database_password": sensitiveString("Password for the database user.", true),
"docker_image": requiredString("libSQL server image to run, for example " +
"`ghcr.io/tursodatabase/libsql-server:latest`."),
"sqld_node": enumStringWithDefault("Role this node plays in a libSQL cluster.", sqldNodes, "primary"),
"sqld_primary_url": optionalString("URL of the primary node, when `sqld_node` is `replica`."),
"enable_namespaces": optionalComputedBool("Serve multiple logical databases from one instance " +
"through libSQL namespaces."),
"command": optionalString("Override the container entrypoint command."),
"env": optionalString("Environment variables in `KEY=value` format, one per line."),
"memory_reservation": optionalString("Soft memory reservation, for example `256m`."),
"memory_limit": optionalString("Hard memory limit, for example `512m`."),
"cpu_reservation": optionalString("Soft CPU reservation, for example `0.5`."),
"cpu_limit": optionalString("Hard CPU limit, for example `1`."),
"replicas": optionalComputedInt("Number of replicas to run."),
"external_port": schema.Int64Attribute{Optional: true, MarkdownDescription: "Host port exposing the HTTP API."},
"external_admin_port": schema.Int64Attribute{Optional: true, MarkdownDescription: "Host port exposing the admin API."},
"external_grpc_port": schema.Int64Attribute{Optional: true, MarkdownDescription: "Host port exposing the gRPC replication endpoint."},
"network_ids": optionalComputedStringList("IDs of additional Docker networks to attach."),
"detach_dokploy_network": optionalComputedBool("Detach the service from the shared `dokploy-network`."),
"health_check_swarm": optionalJSON("Docker Swarm health check configuration, as a JSON object."),
"restart_policy_swarm": optionalJSON("Docker Swarm restart policy, as a JSON object."),
"placement_swarm": optionalJSON("Docker Swarm placement constraints, as a JSON object."),
"update_config_swarm": optionalJSON("Docker Swarm rolling update configuration, as a JSON object."),
"rollback_config_swarm": optionalJSON("Docker Swarm rollback configuration, as a JSON object."),
"mode_swarm": optionalJSON("Docker Swarm service mode, as a JSON object."),
"labels_swarm": optionalJSON("Docker Swarm service labels, as a JSON object."),
"network_swarm": optionalJSON("Docker Swarm network attachments, as a JSON array."),
"endpoint_spec_swarm": optionalJSON("Docker Swarm endpoint specification, as a JSON object."),
"stop_grace_period_swarm": schema.Int64Attribute{Optional: true, MarkdownDescription: "Grace period in nanoseconds before a container is killed."},
"application_status": computedString("Current status reported by Dokploy: `idle`, `running`, `done` or `error`."),
"created_at": computedString("RFC 3339 timestamp of when the database was created."),
},
},
}
}
// generateAppName mirrors how Dokploy names a service: a slug of the display
// name plus a short random suffix, so two databases called "cache" in
// different projects do not collide on the Docker host.
func generateAppName(name string) string {
var slug strings.Builder
lastDash := true
for _, r := range strings.ToLower(name) {
switch {
case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
slug.WriteRune(r)
lastDash = false
case !lastDash:
slug.WriteByte('-')
lastDash = true
}
}
base := strings.Trim(slug.String(), "-")
if base == "" {
base = "libsql"
}
return base + "-" + randomSuffix(6)
}
const suffixAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
func randomSuffix(n int) string {
buf := make([]byte, n)
if _, err := rand.Read(buf); err != nil {
// crypto/rand does not fail in practice; a fixed suffix still yields a
// usable name and Dokploy rejects a genuine collision.
return strings.Repeat("0", n)
}
for i, b := range buf {
buf[i] = suffixAlphabet[int(b)%len(suffixAlphabet)]
}
return string(buf)
}
+96
View File
@@ -0,0 +1,96 @@
package provider
import (
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// Docker networks became first-class in Dokploy v0.30.0. Services attach to
// them through `network_ids` on the application, compose and database
// resources.
type networkModel struct {
ID types.String `tfsdk:"id" dokploy:"networkId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
Driver types.String `tfsdk:"driver" dokploy:"driver"`
Internal types.Bool `tfsdk:"internal" dokploy:"internal"`
Attachable types.Bool `tfsdk:"attachable" dokploy:"attachable"`
EnableIPv4 types.Bool `tfsdk:"enable_ipv4" dokploy:"enableIPv4"`
EnableIPv6 types.Bool `tfsdk:"enable_ipv6" dokploy:"enableIPv6"`
MTU types.Int64 `tfsdk:"mtu" dokploy:"mtu,nullable"`
// IPAM is Docker's address-management block: {"subnet","gateway","ipRange"}.
IPAM jsontypes.Normalized `tfsdk:"ipam" dokploy:"ipam,nullable"`
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,create"`
OrganizationID types.String `tfsdk:"organization_id" dokploy:"organizationId,ro"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func networkResource() ResourceSpec {
// Dokploy exposes no `network.update`: a Docker network's driver, subnet
// and flags are fixed once it exists. Every configurable attribute
// therefore forces replacement.
return ResourceSpec{
Name: "network",
CreateProc: "network.create",
ReadProc: "network.one",
DeleteProc: "network.remove",
NewModel: func() any { return &networkModel{} },
Schema: schema.Schema{
MarkdownDescription: "A Docker network managed by Dokploy.\n\n" +
"Attach services to it with `network_ids` on `dokploy_application`, `dokploy_compose` and the " +
"database resources. Every service also joins the shared `dokploy-network` unless " +
"`detach_dokploy_network` is set.\n\n" +
"~> Docker networks are immutable. Dokploy has no update endpoint, so changing any attribute " +
"replaces the network — which detaches the services currently using it until they redeploy.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique network identifier."),
"name": requiredReplaceString("Name of the Docker network."),
"driver": schema.StringAttribute{
Optional: true,
Computed: true,
MarkdownDescription: "Network driver. Use `overlay` for multi-node Swarm clusters and `bridge` for a single host. Valid values: `bridge`, `overlay`.",
Validators: enumValidator(networkDrivers),
PlanModifiers: requiresReplaceString(),
},
"internal": replaceBool("Isolate the network from external access.", false),
"attachable": replaceBool("Allow standalone containers to attach to an overlay network.", false),
"enable_ipv4": replaceBool("Enable IPv4 address allocation.", true),
"enable_ipv6": replaceBool("Enable IPv6 address allocation.", false),
"mtu": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "Maximum transmission unit for the network. Leave unset to use Docker's default.",
PlanModifiers: []planmodifier.Int64{int64planmodifier.RequiresReplace()},
},
"ipam": schema.StringAttribute{
Optional: true,
CustomType: jsontypes.NormalizedType{},
MarkdownDescription: "Custom IP address management, as a JSON object with `subnet`, `gateway` " +
"and `ipRange` keys. Leave unset to let Docker choose a subnet.",
PlanModifiers: requiresReplaceString(),
},
"server_id": optionalReplaceString("Remote server to create the network on. Omit to use the Dokploy host itself."),
"organization_id": computedString("Organization that owns the network."),
"created_at": computedString("RFC 3339 timestamp of when the network was created."),
},
},
}
}
// replaceBool is an optional boolean with a fixed default that cannot be
// changed in place.
func replaceBool(description string, def bool) schema.BoolAttribute {
return schema.BoolAttribute{
Optional: true,
Computed: true,
MarkdownDescription: description,
Default: booldefault.StaticBool(def),
PlanModifiers: []planmodifier.Bool{boolplanmodifier.RequiresReplace()},
}
}
+7 -3
View File
@@ -29,6 +29,7 @@ type domainModel struct {
StripPath types.Bool `tfsdk:"strip_path" dokploy:"stripPath"`
Middlewares types.List `tfsdk:"middlewares" dokploy:"middlewares"`
ForwardAuthEnabled types.Bool `tfsdk:"forward_auth_enabled" dokploy:"forwardAuthEnabled"`
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled,nullable"`
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"`
@@ -65,9 +66,12 @@ func domainResource() ResourceSpec {
"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."),
"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."),
"enabled": optionalComputedBool("Whether the domain is served. Setting this to `false` removes " +
"the route from Traefik but keeps the certificate, path and middleware configuration intact, " +
"so the domain can be brought back without reconfiguring it."),
"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."),
+131
View File
@@ -0,0 +1,131 @@
package provider
import (
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// DNS providers and vault providers both arrived in Dokploy v0.30.0. They
// share a shape: a name plus a free-form `config` object whose keys depend on
// a `providerType` discriminator nested inside it.
//
// Dokploy masks the credentials in `config` on every read, so the value is
// tagged `noread`: the configured value stays authoritative in state instead
// of being overwritten with asterisks on the next refresh.
// ---------------------------------------------------------- DNS provider
type dnsProviderModel struct {
ID types.String `tfsdk:"id" dokploy:"dnsProviderId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
Config jsontypes.Normalized `tfsdk:"config" dokploy:"config,noread"`
ProviderType types.String `tfsdk:"provider_type" dokploy:"providerType,ro"`
OrganizationID types.String `tfsdk:"organization_id" dokploy:"organizationId,ro"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func dnsProviderResource() ResourceSpec {
return ResourceSpec{
Name: "dns_provider",
CreateProc: "dnsProvider.create",
ReadProc: "dnsProvider.one",
UpdateProc: "dnsProvider.update",
DeleteProc: "dnsProvider.remove",
NewModel: func() any { return &dnsProviderModel{} },
Schema: schema.Schema{
MarkdownDescription: "A DNS provider connection Dokploy uses to create records for domains " +
"automatically.\n\n" +
"`config` is a JSON object whose shape depends on `providerType`:\n\n" +
"```hcl\n" +
"# Cloudflare\n" +
"config = jsonencode({ providerType = \"cloudflare\", apiToken = var.cloudflare_token })\n\n" +
"# AWS Route53\n" +
"config = jsonencode({\n" +
" providerType = \"route53\"\n" +
" accessKeyId = var.aws_access_key_id\n" +
" secretAccessKey = var.aws_secret_access_key\n" +
"})\n" +
"```\n\n" +
"~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value " +
"you configured. Drift in `config` is not detected.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique DNS provider identifier."),
"name": requiredString("Name of the connection. Must be unique within the organization and may " +
"contain only letters, digits, `-` and `_`."),
"config": schema.StringAttribute{
Required: true,
Sensitive: true,
CustomType: jsontypes.NormalizedType{},
MarkdownDescription: "Provider credentials as a JSON object, including the `providerType` discriminator.",
},
"provider_type": computedString("Provider kind derived from `config`: `cloudflare` or `route53`."),
"organization_id": computedString("Organization that owns the connection."),
"created_at": computedString("RFC 3339 timestamp of when the connection was created."),
},
},
}
}
// -------------------------------------------------------- Vault provider
type vaultProviderModel struct {
ID types.String `tfsdk:"id" dokploy:"vaultProviderId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
Config jsontypes.Normalized `tfsdk:"config" dokploy:"config,noread"`
Assignments jsontypes.Normalized `tfsdk:"assignments" dokploy:"assignments"`
ProviderType types.String `tfsdk:"provider_type" dokploy:"providerType,ro"`
OrganizationID types.String `tfsdk:"organization_id" dokploy:"organizationId,ro"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func vaultProviderResource() ResourceSpec {
return ResourceSpec{
Name: "vault_provider",
CreateProc: "vaultProvider.create",
ReadProc: "vaultProvider.one",
UpdateProc: "vaultProvider.update",
DeleteProc: "vaultProvider.remove",
NewModel: func() any { return &vaultProviderModel{} },
Schema: schema.Schema{
MarkdownDescription: "An external secret manager Dokploy resolves environment variables from at " +
"deploy time.\n\n" +
"Reference a secret from any `env` value with `${{vault.<scope>.<key>}}`. The value is fetched " +
"when the deployment runs and is never stored in Dokploy — so rotating it in the vault takes " +
"effect on the next deploy with no Terraform change.\n\n" +
"Supported `providerType` values: `hashicorp` (Vault/OpenBao), `infisical`, `aws` " +
"(Secrets Manager), `doppler`, `azure` (Key Vault) and `scaleway`.\n\n" +
"```hcl\n" +
"config = jsonencode({\n" +
" providerType = \"hashicorp\"\n" +
" url = \"https://vault.example.com\"\n" +
" token = var.vault_token\n" +
" mount = \"secret\"\n" +
"})\n" +
"```\n\n" +
"~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value " +
"you configured. Drift in `config` is not detected.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique vault provider identifier."),
"name": requiredString("Name of the connection, unique within the organization."),
"config": schema.StringAttribute{
Required: true,
Sensitive: true,
CustomType: jsontypes.NormalizedType{},
MarkdownDescription: "Provider credentials as a JSON object, including the `providerType` discriminator.",
},
"assignments": schema.StringAttribute{
Required: true,
CustomType: jsontypes.NormalizedType{},
MarkdownDescription: "JSON array scoping which projects or environments may resolve secrets " +
"from this provider. Pass `jsonencode([])` to leave it unscoped.",
},
"provider_type": computedString("Provider kind derived from `config`."),
"organization_id": computedString("Organization that owns the connection."),
"created_at": computedString("RFC 3339 timestamp of when the connection was created."),
},
},
}
}
+146
View File
@@ -0,0 +1,146 @@
package provider
import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// ------------------------------------------------------------- Schedule
type scheduleModel struct {
ID types.String `tfsdk:"id" dokploy:"scheduleId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
Description types.String `tfsdk:"description" dokploy:"description,nullable"`
CronExpression types.String `tfsdk:"cron_expression" dokploy:"cronExpression"`
Command types.String `tfsdk:"command" dokploy:"command"`
Script types.String `tfsdk:"script" dokploy:"script,nullable"`
ShellType types.String `tfsdk:"shell_type" dokploy:"shellType"`
ScheduleType types.String `tfsdk:"schedule_type" dokploy:"scheduleType"`
Timezone types.String `tfsdk:"timezone" dokploy:"timezone,nullable"`
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled"`
AppName types.String `tfsdk:"app_name" dokploy:"appName,nullable"`
ServiceName types.String `tfsdk:"service_name" dokploy:"serviceName,nullable"`
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,nullable"`
ComposeID types.String `tfsdk:"compose_id" dokploy:"composeId,nullable"`
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,nullable"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func scheduleResource() ResourceSpec {
return ResourceSpec{
Name: "schedule",
CreateProc: "schedule.create",
ReadProc: "schedule.one",
UpdateProc: "schedule.update",
DeleteProc: "schedule.delete",
NewModel: func() any { return &scheduleModel{} },
Schema: schema.Schema{
MarkdownDescription: "A cron job Dokploy runs on a schedule.\n\n" +
"`schedule_type` selects where the command runs:\n\n" +
"* `application` — inside a running application container; set `application_id`.\n" +
"* `compose` — inside one service of a Compose stack; set `compose_id` and `service_name`.\n" +
"* `server` — on a remote server; set `server_id`.\n" +
"* `dokploy-server` — on the Dokploy host itself.\n\n" +
"~> A schedule targeting an application runs inside its container, so the container has to be " +
"running when the cron fires.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique schedule identifier."),
"name": requiredString("Display name of the schedule."),
"description": optionalString("Free-form description."),
"cron_expression": requiredString("Standard five-field cron expression, for example `0 3 * * *`."),
"command": requiredString("Command to run."),
"script": optionalString("Multi-line script to run instead of a single command."),
"shell_type": enumStringWithDefault("Shell used to interpret the command.", shellTypes, "bash"),
"schedule_type": enumStringWithDefault("Where the command runs.", scheduleTypes, "application"),
"timezone": optionalString("IANA timezone the cron expression is evaluated in, for example `Europe/Berlin`."),
"enabled": optionalComputedBool("Whether the schedule is active."),
"app_name": optionalComputedString("Docker service name the schedule targets. Derived by Dokploy when omitted."),
"service_name": optionalString("Service inside a Compose stack to run the command in."),
"application_id": optionalReplaceString("Application this schedule belongs to."),
"compose_id": optionalReplaceString("Compose stack this schedule belongs to."),
"server_id": optionalReplaceString("Server this schedule runs on."),
"created_at": computedString("RFC 3339 timestamp of when the schedule was created."),
},
},
}
}
// -------------------------------------------------------- Volume backup
type volumeBackupModel struct {
ID types.String `tfsdk:"id" dokploy:"volumeBackupId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
VolumeName types.String `tfsdk:"volume_name" dokploy:"volumeName"`
Prefix types.String `tfsdk:"prefix" dokploy:"prefix"`
CronExpression types.String `tfsdk:"cron_expression" dokploy:"cronExpression"`
DestinationID types.String `tfsdk:"destination_id" dokploy:"destinationId"`
ServiceType types.String `tfsdk:"service_type" dokploy:"serviceType"`
AppName types.String `tfsdk:"app_name" dokploy:"appName,nullable"`
ServiceName types.String `tfsdk:"service_name" dokploy:"serviceName,nullable"`
TurnOff types.Bool `tfsdk:"turn_off" dokploy:"turnOff"`
KeepLatestCount types.Int64 `tfsdk:"keep_latest_count" dokploy:"keepLatestCount,nullable"`
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled"`
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,nullable"`
ComposeID types.String `tfsdk:"compose_id" dokploy:"composeId,nullable"`
PostgresID types.String `tfsdk:"postgres_id" dokploy:"postgresId,nullable"`
MySQLID types.String `tfsdk:"mysql_id" dokploy:"mysqlId,nullable"`
MariaDBID types.String `tfsdk:"mariadb_id" dokploy:"mariadbId,nullable"`
MongoID types.String `tfsdk:"mongo_id" dokploy:"mongoId,nullable"`
RedisID types.String `tfsdk:"redis_id" dokploy:"redisId,nullable"`
LibsqlID types.String `tfsdk:"libsql_id" dokploy:"libsqlId,nullable"`
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
}
func volumeBackupResource() ResourceSpec {
return ResourceSpec{
Name: "volume_backup",
CreateProc: "volumeBackups.create",
ReadProc: "volumeBackups.one",
UpdateProc: "volumeBackups.update",
DeleteProc: "volumeBackups.delete",
NewModel: func() any { return &volumeBackupModel{} },
Schema: schema.Schema{
MarkdownDescription: "A scheduled backup of a Docker volume to a configured " +
"`dokploy_destination`.\n\n" +
"This is the counterpart to a `dokploy_mount` with `type = \"volume\"`: the mount gives the " +
"volume a stable name, and this resource copies its contents off the host on a schedule.\n\n" +
"Set exactly one of the `*_id` attributes to say which service owns the volume.",
Attributes: map[string]schema.Attribute{
"id": computedID("Unique volume backup identifier."),
"name": requiredString("Display name of the backup job."),
"volume_name": requiredString("Name of the Docker volume to back up."),
"prefix": requiredString("Path prefix inside the destination bucket, for example `backups/shop/`."),
"cron_expression": requiredString("Standard five-field cron expression, for example `0 4 * * *`."),
"destination_id": requiredString("Backup destination (S3-compatible bucket) to upload to."),
"service_type": enumStringWithDefault("The kind of service that owns the volume.", volumeBackupServiceTypes, "application"),
"app_name": optionalComputedString("Docker service name that owns the volume. Derived by Dokploy when omitted."),
"service_name": optionalString("Service inside a Compose stack that owns the volume."),
"turn_off": optionalComputedBool("Stop the service while the backup runs. Slower, but guarantees a " +
"consistent copy of data that is being written to."),
"keep_latest_count": optionalComputedInt("Number of backups to retain. Older ones are pruned."),
"enabled": optionalComputedBool("Whether the backup schedule is active."),
"application_id": optionalReplaceString("Application that owns the volume."),
"compose_id": optionalReplaceString("Compose stack that owns the volume."),
"postgres_id": optionalReplaceString("PostgreSQL instance that owns the volume."),
"mysql_id": optionalReplaceString("MySQL instance that owns the volume."),
"mariadb_id": optionalReplaceString("MariaDB instance that owns the volume."),
"mongo_id": optionalReplaceString("MongoDB instance that owns the volume."),
"redis_id": optionalReplaceString("Redis instance that owns the volume."),
"libsql_id": optionalReplaceString("libSQL instance that owns the volume."),
"created_at": computedString("RFC 3339 timestamp of when the backup job was created."),
},
},
}
}
+18 -1
View File
@@ -196,6 +196,12 @@ func optionalJSON(description string) schema.StringAttribute {
}
}
// enumValidator is the validator list for a string constrained to a fixed set,
// for attributes assembled by hand rather than through enumString.
func enumValidator(values []string) []validator.String {
return []validator.String{stringvalidator.OneOf(values...)}
}
func joinBackticked(values []string) string {
out := ""
for i, v := range values {
@@ -217,7 +223,18 @@ var (
composeSources = []string{"git", "github", "gitlab", "bitbucket", "gitea", "raw"}
domainTypes = []string{"compose", "application", "preview"}
mountTypes = []string{"bind", "volume", "file"}
serviceTypes = []string{"application", "postgres", "mysql", "mariadb", "mongo", "redis", "compose"}
serviceTypes = []string{"application", "postgres", "mysql", "mariadb", "mongo", "redis", "compose", "libsql"}
protocolTypes = []string{"tcp", "udp"}
publishModes = []string{"ingress", "host"}
// Added in Dokploy v0.30.0.
networkDrivers = []string{"bridge", "overlay"}
shellTypes = []string{"bash", "sh"}
scheduleTypes = []string{"application", "compose", "server", "dokploy-server"}
sqldNodes = []string{"primary", "replica"}
// volumeBackups accepts the service types plus libsql.
volumeBackupServiceTypes = []string{
"application", "postgres", "mysql", "mariadb", "mongo", "redis", "compose", "libsql",
}
)