From 2d1caf6e736f7cf6491e1c2740940790510ca7a1 Mon Sep 17 00:00:00 2001 From: max-voitcov Date: Wed, 26 Aug 2026 00:40:27 +0300 Subject: [PATCH] Cover what Dokploy v0.30 added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 92 ++++++++++ Makefile | 2 +- README.md | 80 ++++++++- docs/index.md | 4 +- docs/resources/application.md | 2 + docs/resources/compose.md | 3 + docs/resources/dns_provider.md | 57 ++++++ docs/resources/domain.md | 1 + docs/resources/libsql.md | 70 ++++++++ docs/resources/network.md | 43 +++++ docs/resources/schedule.md | 53 ++++++ docs/resources/vault_provider.md | 55 ++++++ docs/resources/volume_backup.md | 52 ++++++ examples/v030-features/main.tf | 153 ++++++++++++++++ internal/provider/provider.go | 14 +- internal/provider/resource_application.go | 7 +- internal/provider/resource_compose.go | 13 ++ internal/provider/resource_libsql.go | 206 ++++++++++++++++++++++ internal/provider/resource_network.go | 96 ++++++++++ internal/provider/resource_networking.go | 10 +- internal/provider/resource_providers.go | 131 ++++++++++++++ internal/provider/resource_schedule.go | 146 +++++++++++++++ internal/provider/schema_helpers.go | 19 +- 23 files changed, 1297 insertions(+), 12 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/resources/dns_provider.md create mode 100644 docs/resources/libsql.md create mode 100644 docs/resources/network.md create mode 100644 docs/resources/schedule.md create mode 100644 docs/resources/vault_provider.md create mode 100644 docs/resources/volume_backup.md create mode 100644 examples/v030-features/main.tf create mode 100644 internal/provider/resource_libsql.go create mode 100644 internal/provider/resource_network.go create mode 100644 internal/provider/resource_providers.go create mode 100644 internal/provider/resource_schedule.go diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e9c9563 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,92 @@ +# Changelog + +All notable changes to this provider are documented here. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.0] — 2026-08-26 + +Tracks **Dokploy v0.30.x**. Verified against a live v0.30.2 instance +(597 API operations). + +### Fixed + +- **`dokploy_mount` could silently create a volume that never persisted + anything.** A `type = "volume"` mount with no `volume_name` was accepted by + Dokploy and by this provider. Dokploy renders such a mount as + `{Source: "", Target: }`, and Docker reads an empty source as an + *anonymous* volume: every deploy created a brand-new volume and orphaned the + previous one, so data never survived a redeploy and disk usage grew on every + deployment. + + The provider now rejects that configuration at **plan** time, before anything + is created, with an error explaining the consequence. The same check covers + the two neighbouring cases — `type = "bind"` without `host_path` and + `type = "file"` without `file_path` — and flags a field set for the wrong + `type`, which Dokploy would otherwise ignore silently. + + If you already have such a mount, add a `volume_name`, apply, and redeploy + the service. The data in the current anonymous volume is **not** migrated; + copy it off first if you need it. + +### Added + +New resources, all from Dokploy v0.30.0: + +- **`dokploy_network`** — Docker networks as first-class objects: `bridge` or + `overlay`, with `internal`, `attachable`, IPv4/IPv6, `mtu` and custom `ipam`. + Docker networks are immutable, so every attribute forces replacement. +- **`dokploy_dns_provider`** — a Cloudflare or AWS Route53 connection that lets + Dokploy create DNS records for domains automatically. +- **`dokploy_vault_provider`** — resolve environment variables from an external + secret manager at deploy time with `${{vault..}}`. Supports + HashiCorp Vault/OpenBao, Infisical, AWS Secrets Manager, Doppler, Azure Key + Vault and Scaleway. +- **`dokploy_schedule`** — cron jobs running in an application container, a + Compose service, a remote server, or on the Dokploy host. +- **`dokploy_volume_backup`** — scheduled backups of a Docker volume to a + `dokploy_destination`. The natural companion to a named `dokploy_mount`. +- **`dokploy_libsql`** — the sixth managed database engine, with `primary` and + `replica` `sqld` nodes and libSQL namespaces. + +New attributes on existing resources: + +- `dokploy_domain` gains **`enabled`**, the v0.30.0 enable/disable toggle. It + pulls a route out of Traefik while keeping certificates, paths and middleware + intact, so a domain can be parked and restored without reconfiguring it. +- `dokploy_compose` gains `create_env_file`, `icon` and **`service_networks`**, + which attaches individual services in a stack to specific Docker networks. +- `dokploy_application` gains `icon` and + `preview_require_collaborator_permissions`. +- `dokploy_mount` and `dokploy_volume_backup` accept `libsql` as a + `service_type`. + +### Changed + +- `network_ids` and `detach_dokploy_network` are now documented as the + supported way to control service networking. Compose's + `isolated_deployment` is **deprecated upstream** in v0.30.0 — attaching + networks per service covers the same ground and survives restarts. The + attribute still exists and still works; prefer `network_ids` for new + configurations. + +### Notes + +- Dokploy's `libsql.create` requires eleven keys to be present even when null + is the only sensible value, and does not generate a service name. The + provider fills both in, so `app_name` stays optional as it is for every other + database. +- `config` on `dokploy_dns_provider` and `dokploy_vault_provider` is masked by + Dokploy on read. Terraform keeps the value you configured and does **not** + detect drift in those credentials. + +## [0.1.0] — 2026-08-09 + +Initial release. Projects, environments, applications, Compose stacks, five +managed databases (PostgreSQL, MySQL, MariaDB, MongoDB, Redis), domains, +mounts, ports, redirects, basic auth, registries, SSH keys, certificates and +backup destinations, plus five data sources and a `web-service` module. + +[0.2.0]: https://gitea.coolify.vojtkov.dev/usr_unknown/terraform-provider-dokploy/releases/tag/v0.2.0 +[0.1.0]: https://gitea.coolify.vojtkov.dev/usr_unknown/terraform-provider-dokploy/releases/tag/v0.1.0 diff --git a/Makefile b/Makefile index ee84e5e..24f864d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ BINARY := terraform-provider-dokploy -VERSION ?= 0.1.0 +VERSION ?= 0.2.0 NAMESPACE := maxvojtkov NAME := dokploy diff --git a/README.md b/README.md index 6dc3e6d..77b4f38 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ resource "dokploy_domain" "api" { - [Deployments are not managed](#deployments-are-not-managed) - [Importing existing infrastructure](#importing-existing-infrastructure) - [Using this from Pulumi](#using-this-from-pulumi) +- [Volumes that actually persist](#volumes-that-actually-persist) - [Known API quirks](#known-api-quirks) - [Development](#development) @@ -70,7 +71,7 @@ endpoint that can serve the provider protocol `terraform init` speaks. ### From a release ```bash -VERSION=0.1.0 +VERSION=0.2.0 OS_ARCH="$(go env GOOS)_$(go env GOARCH)" BASE=https://gitea.coolify.vojtkov.dev/usr_unknown/terraform-provider-dokploy/releases/download @@ -92,7 +93,7 @@ make install ``` That writes the binary to -`~/.local/share/terraform/plugins/registry.terraform.io/maxvojtkov/dokploy/0.1.0/_/` +`~/.local/share/terraform/plugins/registry.terraform.io/maxvojtkov/dokploy/0.2.0/_/` and prints the CLI configuration to add to `~/.terraformrc`: ```hcl @@ -114,7 +115,7 @@ terraform { required_providers { dokploy = { source = "maxvojtkov/dokploy" - version = "0.1.0" + version = "0.2.0" } } } @@ -202,6 +203,7 @@ Full reference documentation lives in [`docs/`](./docs). | [`dokploy_mariadb`](docs/resources/mariadb.md) | Managed MariaDB | | [`dokploy_mongo`](docs/resources/mongo.md) | Managed MongoDB | | [`dokploy_redis`](docs/resources/redis.md) | Managed Redis | +| [`dokploy_libsql`](docs/resources/libsql.md) | Managed libSQL (`sqld`) | | [`dokploy_domain`](docs/resources/domain.md) | A hostname routed through Traefik | | [`dokploy_mount`](docs/resources/mount.md) | Volume, bind mount or config file | | [`dokploy_port`](docs/resources/port.md) | A port published straight onto the host | @@ -211,6 +213,11 @@ Full reference documentation lives in [`docs/`](./docs). | [`dokploy_ssh_key`](docs/resources/ssh_key.md) | SSH key pair for private Git and remote servers | | [`dokploy_certificate`](docs/resources/certificate.md) | An uploaded TLS certificate | | [`dokploy_destination`](docs/resources/destination.md) | S3-compatible backup destination | +| [`dokploy_network`](docs/resources/network.md) | A Docker network services attach to | +| [`dokploy_schedule`](docs/resources/schedule.md) | A cron job run in a container or on a server | +| [`dokploy_volume_backup`](docs/resources/volume_backup.md) | A scheduled backup of a Docker volume | +| [`dokploy_dns_provider`](docs/resources/dns_provider.md) | Cloudflare or Route53, for automatic DNS records | +| [`dokploy_vault_provider`](docs/resources/vault_provider.md) | An external secret manager for deploy-time env | ### Data sources @@ -411,6 +418,56 @@ p := tfshim.NewProvider("0.1.0") // a plugin-framework provider.Provider Keep `shim.NewProvider` stable — it is this repository's only public Go API. +## Volumes that actually persist + +A `dokploy_mount` with `type = "volume"` **must** set `volume_name`: + +```hcl +resource "dokploy_mount" "data" { + type = "volume" + volume_name = "shop-uploads" # required — see below + mount_path = "/app/uploads" + service_type = "application" + service_id = dokploy_application.api.id +} +``` + +Dokploy turns a mount into a Docker mount with +`{Source: volumeName || "", Target: mountPath}`. When `volumeName` is null the +source is the empty string, and Docker reads an empty source as an *anonymous* +volume — a new one on every single deploy, with the previous one left orphaned +on disk. Nothing errors: the service starts, the path is writable, and the data +is gone again after the next deployment while disk usage climbs. + +Dokploy's API accepts that mount without complaint, so this provider rejects it +at plan time instead: + +``` +Error: Missing volume_name + + with dokploy_mount.data, + on main.tf line 1, in resource "dokploy_mount" "data": + +`volume_name` must be set to a non-empty value when `type` is `volume`. + +Dokploy passes an unset `volume_name` to Docker as an empty source, which +creates a new anonymous volume on every deploy. The data written to the +previous volume is orphaned and never reused, so the mount silently does not +persist anything. +``` + +The same check covers `type = "bind"` without `host_path` and `type = "file"` +without `file_path`, and it rejects a field set against the wrong `type` — +`volume_name` on a bind mount, say — which Dokploy would otherwise ignore. + +**If you already have such a mount**, adding `volume_name` and redeploying +gives you a persistent volume from that point on. The contents of the current +anonymous volume are not migrated; copy the data off the host first if you +need it. + +Pair a named volume with [`dokploy_volume_backup`](docs/resources/volume_backup.md) +to get it off the host on a schedule. + ## Known API quirks These are properties of the Dokploy API that the provider works around; they @@ -434,6 +491,23 @@ explain behaviour that would otherwise look surprising. `environment.create`) reject it. The provider tracks this per field. - **Basic auth passwords are never returned.** `dokploy_security.password` keeps whatever you configured; drift in that one field cannot be detected. + DNS and vault provider credentials behave the same way: Dokploy masks + `config` on read, so `dokploy_dns_provider.config` and + `dokploy_vault_provider.config` keep the configured value and are not + checked for drift. +- **`libsql.create` is the strictest endpoint in the API.** It requires eleven + keys to be *present* — several only meaningfully `null` — declines to + generate a service name the way every other engine does, and then returns + `true` instead of the created row. The provider fills in the nulls, derives + an `app_name` from `name`, and finds the new ID by diffing the environment's + libSQL list, so `dokploy_libsql` behaves like the other databases. +- **A `volume` mount with no name is silently anonymous.** Covered in + [Volumes that actually persist](#volumes-that-actually-persist); the provider + rejects it at plan time. +- **Docker networks cannot be updated.** Dokploy exposes no `network.update`, + matching Docker itself, so every attribute of `dokploy_network` forces + replacement. Replacing a network detaches the services using it until they + are redeployed. ## Development diff --git a/docs/index.md b/docs/index.md index c27873e..fac8b23 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,12 +2,12 @@ # generated by https://github.com/hashicorp/terraform-plugin-docs page_title: "dokploy Provider" description: |- - Manage Dokploy https://dokploy.com projects, environments, applications, compose stacks, databases and networking with Terraform. + Manage Dokploy https://dokploy.com projects, environments, applications, compose stacks, databases, Docker networks and networking with Terraform. --- # dokploy Provider -Manage [Dokploy](https://dokploy.com) projects, environments, applications, compose stacks, databases and networking with Terraform. +Manage [Dokploy](https://dokploy.com) projects, environments, applications, compose stacks, databases, Docker networks and networking with Terraform. diff --git a/docs/resources/application.md b/docs/resources/application.md index 6c48ae1..9febf5d 100644 --- a/docs/resources/application.md +++ b/docs/resources/application.md @@ -78,6 +78,7 @@ Dokploy's `application.create` endpoint accepts only a handful of fields, so thi - `gitlab_repository` (String) GitLab repository name. - `health_check_swarm` (String) Docker Swarm health check configuration, as a JSON object. - `heroku_version` (String) Heroku buildpack stack version. +- `icon` (String) Icon shown next to the service in the Dokploy UI. - `is_preview_deployments_active` (Boolean) Build a preview deployment for each pull request. - `is_static_spa` (Boolean) Serve a static build as a single-page application. - `labels_swarm` (String) Docker Swarm service labels, as a JSON object. @@ -99,6 +100,7 @@ Dokploy's `application.create` endpoint accepts only a handful of fields, so thi - `preview_limit` (Number) Maximum number of concurrent preview deployments. - `preview_path` (String) Base path for preview deployments. - `preview_port` (Number) Container port exposed by preview deployments. +- `preview_require_collaborator_permissions` (Boolean) Only build previews for pull requests opened by users with repository collaborator permissions. - `preview_wildcard` (String) Wildcard domain used to expose preview deployments. - `publish_directory` (String) Directory served when `build_type` is `static`. - `railpack_version` (String) Railpack version. diff --git a/docs/resources/compose.md b/docs/resources/compose.md index 1c0c0eb..39296d0 100644 --- a/docs/resources/compose.md +++ b/docs/resources/compose.md @@ -40,6 +40,7 @@ Set `compose_file` to manage the stack definition inline (with `source_type = "r - `compose_file` (String) Inline Compose file contents. Used when `source_type` is `raw`. - `compose_path` (String) Path to the Compose file within the repository. - `compose_type` (String) Whether to run the stack with Docker Compose or Docker Swarm. Valid values: `docker-compose`, `stack`. +- `create_env_file` (Boolean) Write the environment variables to a `.env` file next to the Compose file. - `custom_git_branch` (String) Branch to deploy for a custom Git remote. - `custom_git_ssh_key_id` (String) SSH key used to clone a private custom Git remote. - `custom_git_url` (String) Git remote URL, when `source_type` is `git`. @@ -58,12 +59,14 @@ Set `compose_file` to manage the stack definition inline (with `source_type = "r - `gitlab_path_namespace` (String) Full GitLab namespace path. - `gitlab_project_id` (Number) Numeric GitLab project ID. - `gitlab_repository` (String) GitLab repository name. +- `icon` (String) Icon shown next to the stack in the Dokploy UI. - `isolated_deployment` (Boolean) Run the stack on its own isolated Docker network. - `isolated_deployments_volume` (Boolean) Prefix volume names for isolated deployments. Retained for backwards compatibility. - `owner` (String) GitHub repository owner. - `randomize` (Boolean) Append a random suffix to service and volume names. - `repository` (String) GitHub repository name. - `server_id` (String) Remote server to deploy on. Omit to use the Dokploy host itself. +- `service_networks` (String) Per-service Docker network attachments, as a JSON object mapping each service name in the stack to an array of network IDs. - `source_type` (String) Where the Compose file comes from. Valid values: `git`, `github`, `gitlab`, `bitbucket`, `gitea`, `raw`. - `suffix` (String) Suffix appended to generated resource names. - `trigger_type` (String) What triggers an automatic deployment. Valid values: `push`, `tag`. diff --git a/docs/resources/dns_provider.md b/docs/resources/dns_provider.md new file mode 100644 index 0000000..0bfd6b7 --- /dev/null +++ b/docs/resources/dns_provider.md @@ -0,0 +1,57 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokploy_dns_provider Resource - dokploy" +subcategory: "" +description: |- + A DNS provider connection Dokploy uses to create records for domains automatically. + config is a JSON object whose shape depends on providerType: + + # Cloudflare + config = jsonencode({ providerType = "cloudflare", apiToken = var.cloudflare_token }) + + # AWS Route53 + config = jsonencode({ + providerType = "route53" + accessKeyId = var.aws_access_key_id + secretAccessKey = var.aws_secret_access_key + }) + + ~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value you configured. Drift in config is not detected. +--- + +# dokploy_dns_provider (Resource) + +A DNS provider connection Dokploy uses to create records for domains automatically. + +`config` is a JSON object whose shape depends on `providerType`: + +```hcl +# Cloudflare +config = jsonencode({ providerType = "cloudflare", apiToken = var.cloudflare_token }) + +# AWS Route53 +config = jsonencode({ + providerType = "route53" + accessKeyId = var.aws_access_key_id + secretAccessKey = var.aws_secret_access_key +}) +``` + +~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value you configured. Drift in `config` is not detected. + + + + +## Schema + +### Required + +- `config` (String, Sensitive) Provider credentials as a JSON object, including the `providerType` discriminator. +- `name` (String) Name of the connection. Must be unique within the organization and may contain only letters, digits, `-` and `_`. + +### Read-Only + +- `created_at` (String) RFC 3339 timestamp of when the connection was created. +- `id` (String) Unique DNS provider identifier. +- `organization_id` (String) Organization that owns the connection. +- `provider_type` (String) Provider kind derived from `config`: `cloudflare` or `route53`. diff --git a/docs/resources/domain.md b/docs/resources/domain.md index 4dc31f8..8c220dd 100644 --- a/docs/resources/domain.md +++ b/docs/resources/domain.md @@ -30,6 +30,7 @@ Set exactly one of `application_id` or `compose_id`. When targeting a Compose st - `custom_cert_resolver` (String) Traefik certificate resolver name, when `certificate_type` is `custom`. - `custom_entrypoint` (String) Traefik entrypoint to bind, when not using the defaults. - `domain_type` (String) What kind of target this domain points at. Valid values: `compose`, `application`, `preview`. +- `enabled` (Boolean) 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. - `forward_auth_enabled` (Boolean) Protect this domain with Dokploy's forward auth. - `https` (Boolean) Serve the domain over HTTPS and redirect HTTP traffic to it. - `internal_path` (String) Path the request is rewritten to before it reaches the container, defaults to `/`. diff --git a/docs/resources/libsql.md b/docs/resources/libsql.md new file mode 100644 index 0000000..a0b7218 --- /dev/null +++ b/docs/resources/libsql.md @@ -0,0 +1,70 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokploy_libsql Resource - dokploy" +subcategory: "" +description: |- + A managed libSQL instance running on Dokploy. + ~> Creating this resource provisions the service definition but does not start a deployment. Deploy it from the Dokploy UI or CLI. + ~> Credentials are stored in Terraform state. Use a state backend with encryption at rest. + libSQL runs as a sqld server. A primary node owns the data; a replica node follows a primary named by sqld_primary_url. +--- + +# dokploy_libsql (Resource) + +A managed libSQL instance running on Dokploy. + +~> Creating this resource provisions the service definition but does **not** start a deployment. Deploy it from the Dokploy UI or CLI. + +~> Credentials are stored in Terraform state. Use a state backend with encryption at rest. + +libSQL runs as a `sqld` server. A `primary` node owns the data; a `replica` node follows a primary named by `sqld_primary_url`. + + + + +## Schema + +### Required + +- `database_password` (String, Sensitive) Password for the database user. +- `database_user` (String) Database user to create. +- `docker_image` (String) libSQL server image to run, for example `ghcr.io/tursodatabase/libsql-server:latest`. +- `environment_id` (String) Environment this database belongs to. +- `name` (String) Display name of the database. + +### Optional + +- `app_name` (String) Unique Docker service name. Generated from `name` when omitted, because Dokploy's libSQL endpoint does not generate one. Changing it forces a new database. +- `command` (String) Override the container entrypoint command. +- `cpu_limit` (String) Hard CPU limit, for example `1`. +- `cpu_reservation` (String) Soft CPU reservation, for example `0.5`. +- `description` (String) Free-form description. +- `detach_dokploy_network` (Boolean) Detach the service from the shared `dokploy-network`. +- `enable_namespaces` (Boolean) Serve multiple logical databases from one instance through libSQL namespaces. +- `endpoint_spec_swarm` (String) Docker Swarm endpoint specification, as a JSON object. +- `env` (String) Environment variables in `KEY=value` format, one per line. +- `external_admin_port` (Number) Host port exposing the admin API. +- `external_grpc_port` (Number) Host port exposing the gRPC replication endpoint. +- `external_port` (Number) Host port exposing the HTTP API. +- `health_check_swarm` (String) Docker Swarm health check configuration, as a JSON object. +- `labels_swarm` (String) Docker Swarm service labels, as a JSON object. +- `memory_limit` (String) Hard memory limit, for example `512m`. +- `memory_reservation` (String) Soft memory reservation, for example `256m`. +- `mode_swarm` (String) Docker Swarm service mode, as a JSON object. +- `network_ids` (List of String) IDs of additional Docker networks to attach. +- `network_swarm` (String) Docker Swarm network attachments, as a JSON array. +- `placement_swarm` (String) Docker Swarm placement constraints, as a JSON object. +- `replicas` (Number) Number of replicas to run. +- `restart_policy_swarm` (String) Docker Swarm restart policy, as a JSON object. +- `rollback_config_swarm` (String) Docker Swarm rollback configuration, as a JSON object. +- `server_id` (String) Remote server to deploy on. Omit to use the Dokploy host itself. +- `sqld_node` (String) Role this node plays in a libSQL cluster. Valid values: `primary`, `replica`. Defaults to `primary`. +- `sqld_primary_url` (String) URL of the primary node, when `sqld_node` is `replica`. +- `stop_grace_period_swarm` (Number) Grace period in nanoseconds before a container is killed. +- `update_config_swarm` (String) Docker Swarm rolling update configuration, as a JSON object. + +### Read-Only + +- `application_status` (String) Current status reported by Dokploy: `idle`, `running`, `done` or `error`. +- `created_at` (String) RFC 3339 timestamp of when the database was created. +- `id` (String) Unique libSQL identifier. diff --git a/docs/resources/network.md b/docs/resources/network.md new file mode 100644 index 0000000..b03850a --- /dev/null +++ b/docs/resources/network.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokploy_network Resource - dokploy" +subcategory: "" +description: |- + A Docker network managed by Dokploy. + 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. + ~> 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. +--- + +# dokploy_network (Resource) + +A Docker network managed by Dokploy. + +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. + +~> 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. + + + + +## Schema + +### Required + +- `name` (String) Name of the Docker network. + +### Optional + +- `attachable` (Boolean) Allow standalone containers to attach to an overlay network. +- `driver` (String) Network driver. Use `overlay` for multi-node Swarm clusters and `bridge` for a single host. Valid values: `bridge`, `overlay`. +- `enable_ipv4` (Boolean) Enable IPv4 address allocation. +- `enable_ipv6` (Boolean) Enable IPv6 address allocation. +- `internal` (Boolean) Isolate the network from external access. +- `ipam` (String) Custom IP address management, as a JSON object with `subnet`, `gateway` and `ipRange` keys. Leave unset to let Docker choose a subnet. +- `mtu` (Number) Maximum transmission unit for the network. Leave unset to use Docker's default. +- `server_id` (String) Remote server to create the network on. Omit to use the Dokploy host itself. + +### Read-Only + +- `created_at` (String) RFC 3339 timestamp of when the network was created. +- `id` (String) Unique network identifier. +- `organization_id` (String) Organization that owns the network. diff --git a/docs/resources/schedule.md b/docs/resources/schedule.md new file mode 100644 index 0000000..ff94aa0 --- /dev/null +++ b/docs/resources/schedule.md @@ -0,0 +1,53 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokploy_schedule Resource - dokploy" +subcategory: "" +description: |- + A cron job Dokploy runs on a schedule. + schedule_type selects where the command runs: + application — inside a running application container; set application_id.compose — inside one service of a Compose stack; set compose_id and service_name.server — on a remote server; set server_id.dokploy-server — on the Dokploy host itself. + ~> A schedule targeting an application runs inside its container, so the container has to be running when the cron fires. +--- + +# dokploy_schedule (Resource) + +A cron job Dokploy runs on a schedule. + +`schedule_type` selects where the command runs: + +* `application` — inside a running application container; set `application_id`. +* `compose` — inside one service of a Compose stack; set `compose_id` and `service_name`. +* `server` — on a remote server; set `server_id`. +* `dokploy-server` — on the Dokploy host itself. + +~> A schedule targeting an application runs inside its container, so the container has to be running when the cron fires. + + + + +## Schema + +### Required + +- `command` (String) Command to run. +- `cron_expression` (String) Standard five-field cron expression, for example `0 3 * * *`. +- `name` (String) Display name of the schedule. + +### Optional + +- `app_name` (String) Docker service name the schedule targets. Derived by Dokploy when omitted. +- `application_id` (String) Application this schedule belongs to. +- `compose_id` (String) Compose stack this schedule belongs to. +- `description` (String) Free-form description. +- `enabled` (Boolean) Whether the schedule is active. +- `schedule_type` (String) Where the command runs. Valid values: `application`, `compose`, `server`, `dokploy-server`. Defaults to `application`. +- `script` (String) Multi-line script to run instead of a single command. +- `server_id` (String) Server this schedule runs on. +- `service_name` (String) Service inside a Compose stack to run the command in. +- `shell_type` (String) Shell used to interpret the command. Valid values: `bash`, `sh`. Defaults to `bash`. +- `timezone` (String) IANA timezone the cron expression is evaluated in, for example `Europe/Berlin`. + +### Read-Only + +- `created_at` (String) RFC 3339 timestamp of when the schedule was created. +- `id` (String) Unique schedule identifier. diff --git a/docs/resources/vault_provider.md b/docs/resources/vault_provider.md new file mode 100644 index 0000000..c0046a2 --- /dev/null +++ b/docs/resources/vault_provider.md @@ -0,0 +1,55 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokploy_vault_provider Resource - dokploy" +subcategory: "" +description: |- + An external secret manager Dokploy resolves environment variables from at deploy time. + Reference a secret from any env value with ${{vault..}}. 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. + Supported providerType values: hashicorp (Vault/OpenBao), infisical, aws (Secrets Manager), doppler, azure (Key Vault) and scaleway. + + config = jsonencode({ + providerType = "hashicorp" + url = "https://vault.example.com" + token = var.vault_token + mount = "secret" + }) + + ~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value you configured. Drift in config is not detected. +--- + +# dokploy_vault_provider (Resource) + +An external secret manager Dokploy resolves environment variables from at deploy time. + +Reference a secret from any `env` value with `${{vault..}}`. 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. + +Supported `providerType` values: `hashicorp` (Vault/OpenBao), `infisical`, `aws` (Secrets Manager), `doppler`, `azure` (Key Vault) and `scaleway`. + +```hcl +config = jsonencode({ + providerType = "hashicorp" + url = "https://vault.example.com" + token = var.vault_token + mount = "secret" +}) +``` + +~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value you configured. Drift in `config` is not detected. + + + + +## Schema + +### Required + +- `assignments` (String) JSON array scoping which projects or environments may resolve secrets from this provider. Pass `jsonencode([])` to leave it unscoped. +- `config` (String, Sensitive) Provider credentials as a JSON object, including the `providerType` discriminator. +- `name` (String) Name of the connection, unique within the organization. + +### Read-Only + +- `created_at` (String) RFC 3339 timestamp of when the connection was created. +- `id` (String) Unique vault provider identifier. +- `organization_id` (String) Organization that owns the connection. +- `provider_type` (String) Provider kind derived from `config`. diff --git a/docs/resources/volume_backup.md b/docs/resources/volume_backup.md new file mode 100644 index 0000000..647d920 --- /dev/null +++ b/docs/resources/volume_backup.md @@ -0,0 +1,52 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "dokploy_volume_backup Resource - dokploy" +subcategory: "" +description: |- + A scheduled backup of a Docker volume to a configured dokploy_destination. + 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. + Set exactly one of the *_id attributes to say which service owns the volume. +--- + +# dokploy_volume_backup (Resource) + +A scheduled backup of a Docker volume to a configured `dokploy_destination`. + +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. + +Set exactly one of the `*_id` attributes to say which service owns the volume. + + + + +## Schema + +### Required + +- `cron_expression` (String) Standard five-field cron expression, for example `0 4 * * *`. +- `destination_id` (String) Backup destination (S3-compatible bucket) to upload to. +- `name` (String) Display name of the backup job. +- `prefix` (String) Path prefix inside the destination bucket, for example `backups/shop/`. +- `volume_name` (String) Name of the Docker volume to back up. + +### Optional + +- `app_name` (String) Docker service name that owns the volume. Derived by Dokploy when omitted. +- `application_id` (String) Application that owns the volume. +- `compose_id` (String) Compose stack that owns the volume. +- `enabled` (Boolean) Whether the backup schedule is active. +- `keep_latest_count` (Number) Number of backups to retain. Older ones are pruned. +- `libsql_id` (String) libSQL instance that owns the volume. +- `mariadb_id` (String) MariaDB instance that owns the volume. +- `mongo_id` (String) MongoDB instance that owns the volume. +- `mysql_id` (String) MySQL instance that owns the volume. +- `postgres_id` (String) PostgreSQL instance that owns the volume. +- `redis_id` (String) Redis instance that owns the volume. +- `service_name` (String) Service inside a Compose stack that owns the volume. +- `service_type` (String) The kind of service that owns the volume. Valid values: `application`, `postgres`, `mysql`, `mariadb`, `mongo`, `redis`, `compose`, `libsql`. Defaults to `application`. +- `turn_off` (Boolean) Stop the service while the backup runs. Slower, but guarantees a consistent copy of data that is being written to. + +### Read-Only + +- `created_at` (String) RFC 3339 timestamp of when the backup job was created. +- `id` (String) Unique volume backup identifier. diff --git a/examples/v030-features/main.tf b/examples/v030-features/main.tf new file mode 100644 index 0000000..9848437 --- /dev/null +++ b/examples/v030-features/main.tf @@ -0,0 +1,153 @@ +# Features introduced in Dokploy v0.30.0: Docker networks, vault-backed +# environment variables, scheduled jobs, and a volume that actually persists. + +terraform { + required_providers { + dokploy = { + source = "maxvojtkov/dokploy" + version = "~> 0.2.0" + } + } +} + +provider "dokploy" { + # host and api_key come from DOKPLOY_HOST and DOKPLOY_API_KEY +} + +variable "vault_token" { + type = string + sensitive = true +} + +resource "dokploy_project" "shop" { + name = "shop" + description = "Storefront and its backing services" +} + +# --------------------------------------------------------------- Networking + +# A private overlay network. Only the services attached to it can reach each +# other over it — the database never becomes reachable from unrelated +# services that merely share the default dokploy-network. +resource "dokploy_network" "backend" { + name = "shop-backend" + driver = "overlay" + attachable = true + internal = false +} + +# ------------------------------------------------------------------ Secrets + +# Environment values are resolved from Vault when the deployment runs, so +# rotating a secret takes effect on the next deploy with no Terraform change +# and no secret in Terraform state. +resource "dokploy_vault_provider" "prod" { + name = "production-vault" + + config = jsonencode({ + providerType = "hashicorp" + url = "https://vault.example.com" + token = var.vault_token + mount = "secret" + }) + + assignments = jsonencode([]) +} + +# ---------------------------------------------------------------- Services + +resource "dokploy_postgres" "db" { + name = "shop-db" + environment_id = dokploy_project.shop.default_environment_id + docker_image = "postgres:16-alpine" + database_name = "shop" + database_user = "shop" + database_password = "set-me-from-a-variable" + + network_ids = [dokploy_network.backend.id] +} + +resource "dokploy_application" "api" { + name = "api" + environment_id = dokploy_project.shop.default_environment_id + source_type = "docker" + docker_image = "ghcr.io/acme/api:1.4.0" + + network_ids = [dokploy_network.backend.id] + + # Resolved from the vault provider above at deploy time. + env = <<-EOT + DATABASE_URL=postgresql://shop:$${{vault.production.db_password}}@${dokploy_postgres.db.app_name}:5432/shop + STRIPE_KEY=$${{vault.production.stripe_key}} + EOT +} + +# -------------------------------------------------------- Persistent volume + +# volume_name is what makes this persist. Without it Dokploy hands Docker an +# empty source and every deploy gets a fresh anonymous volume — the provider +# rejects that at plan time. +resource "dokploy_mount" "uploads" { + type = "volume" + volume_name = "shop-uploads" + mount_path = "/app/uploads" + service_type = "application" + service_id = dokploy_application.api.id +} + +resource "dokploy_destination" "backups" { + name = "s3-backups" + provider_name = "s3" + access_key = "set-me" + secret_access_key = "set-me" + bucket = "shop-backups" + region = "eu-central-1" + endpoint = "https://s3.eu-central-1.amazonaws.com" +} + +# The named volume is only durable if it also leaves the host. +resource "dokploy_volume_backup" "uploads" { + name = "uploads-nightly" + volume_name = dokploy_mount.uploads.volume_name + prefix = "shop/uploads/" + cron_expression = "0 4 * * *" + destination_id = dokploy_destination.backups.id + + service_type = "application" + application_id = dokploy_application.api.id + + keep_latest_count = 14 +} + +# ---------------------------------------------------------------- Schedules + +resource "dokploy_schedule" "prune_sessions" { + name = "prune-sessions" + description = "Drop expired sessions every night" + schedule_type = "application" + application_id = dokploy_application.api.id + cron_expression = "0 2 * * *" + command = "node scripts/prune-sessions.js" + timezone = "Europe/Berlin" +} + +# --------------------------------------------------------------------- DNS + +resource "dokploy_dns_provider" "cloudflare" { + name = "cloudflare" + config = jsonencode({ + providerType = "cloudflare" + apiToken = "set-me-from-a-variable" + }) +} + +resource "dokploy_domain" "api" { + application_id = dokploy_application.api.id + host = "api.example.com" + port = 3000 + https = true + certificate_type = "letsencrypt" + + # A domain can be parked without losing its configuration. + enabled = true +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c83fd57..c90d90a 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -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()), diff --git a/internal/provider/resource_application.go b/internal/provider/resource_application.go index 6081c57..7820603 100644 --- a/internal/provider/resource_application.go +++ b/internal/provider/resource_application.go @@ -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."), diff --git a/internal/provider/resource_compose.go b/internal/provider/resource_compose.go index 040bb76..039c5b5 100644 --- a/internal/provider/resource_compose.go +++ b/internal/provider/resource_compose.go @@ -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."), diff --git a/internal/provider/resource_libsql.go b/internal/provider/resource_libsql.go new file mode 100644 index 0000000..cb784f3 --- /dev/null +++ b/internal/provider/resource_libsql.go @@ -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) +} diff --git a/internal/provider/resource_network.go b/internal/provider/resource_network.go new file mode 100644 index 0000000..32cabd2 --- /dev/null +++ b/internal/provider/resource_network.go @@ -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()}, + } +} diff --git a/internal/provider/resource_networking.go b/internal/provider/resource_networking.go index cee551b..61787f7 100644 --- a/internal/provider/resource_networking.go +++ b/internal/provider/resource_networking.go @@ -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."), diff --git a/internal/provider/resource_providers.go b/internal/provider/resource_providers.go new file mode 100644 index 0000000..8d58c5b --- /dev/null +++ b/internal/provider/resource_providers.go @@ -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..}}`. 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."), + }, + }, + } +} diff --git a/internal/provider/resource_schedule.go b/internal/provider/resource_schedule.go new file mode 100644 index 0000000..7ec8cfd --- /dev/null +++ b/internal/provider/resource_schedule.go @@ -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."), + }, + }, + } +} diff --git a/internal/provider/schema_helpers.go b/internal/provider/schema_helpers.go index 3639dd4..e30ba50 100644 --- a/internal/provider/schema_helpers.go +++ b/internal/provider/schema_helpers.go @@ -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", + } )