Cover what Dokploy v0.30 added
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:
@@ -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()),
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()},
|
||||
}
|
||||
}
|
||||
@@ -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."),
|
||||
|
||||
@@ -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."),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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."),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user