Files
max-voitcov 2d1caf6e73
build / build (push) Successful in 3m56s
release / release (push) Successful in 15m2s
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.
2026-08-26 00:40:27 +03:00

207 lines
10 KiB
Go

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)
}