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:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user