Compare commits
3
Commits
v0.1.1
...
2d1caf6e73
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d1caf6e73 | ||
|
|
3ddce62647 | ||
|
|
0890384552 |
@@ -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: <mount_path>}`, 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.<scope>.<key>}}`. 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
|
||||
@@ -1,5 +1,5 @@
|
||||
BINARY := terraform-provider-dokploy
|
||||
VERSION ?= 0.1.0
|
||||
VERSION ?= 0.2.0
|
||||
NAMESPACE := maxvojtkov
|
||||
NAME := dokploy
|
||||
|
||||
|
||||
@@ -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/<os>_<arch>/`
|
||||
`~/.local/share/terraform/plugins/registry.terraform.io/maxvojtkov/dokploy/0.2.0/<os>_<arch>/`
|
||||
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
|
||||
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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 generated by tfplugindocs -->
|
||||
## 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`.
|
||||
@@ -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 `/`.
|
||||
|
||||
@@ -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 generated by tfplugindocs -->
|
||||
## 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.
|
||||
@@ -5,6 +5,7 @@ subcategory: ""
|
||||
description: |-
|
||||
A volume, bind mount, or config file attached to a Dokploy service.
|
||||
type = "volume" — a named Docker volume; set volume_name.type = "bind" — a path on the host; set host_path.type = "file" — a file rendered from content; set file_path.
|
||||
~> A volume mount must set volume_name. Dokploy hands an unset name to Docker as an empty source, which creates a fresh anonymous volume on every deploy and orphans the previous one — the data never survives a redeploy. The provider rejects that combination at plan time.
|
||||
---
|
||||
|
||||
# dokploy_mount (Resource)
|
||||
@@ -15,6 +16,8 @@ A volume, bind mount, or config file attached to a Dokploy service.
|
||||
* `type = "bind"` — a path on the host; set `host_path`.
|
||||
* `type = "file"` — a file rendered from `content`; set `file_path`.
|
||||
|
||||
~> **A `volume` mount must set `volume_name`.** Dokploy hands an unset name to Docker as an empty source, which creates a fresh anonymous volume on every deploy and orphans the previous one — the data never survives a redeploy. The provider rejects that combination at plan time.
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
@@ -24,7 +27,7 @@ A volume, bind mount, or config file attached to a Dokploy service.
|
||||
|
||||
- `mount_path` (String) Path inside the container where the mount appears.
|
||||
- `service_id` (String) ID of the service this mount attaches to. Must match `service_type` — an application ID, a compose ID, a postgres ID, and so on.
|
||||
- `service_type` (String) The kind of service this mount attaches to. Valid values: `application`, `postgres`, `mysql`, `mariadb`, `mongo`, `redis`, `compose`.
|
||||
- `service_type` (String) The kind of service this mount attaches to. Valid values: `application`, `postgres`, `mysql`, `mariadb`, `mongo`, `redis`, `compose`, `libsql`.
|
||||
- `type` (String) The kind of mount to create. Valid values: `bind`, `volume`, `file`.
|
||||
|
||||
### Optional
|
||||
|
||||
@@ -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 generated by tfplugindocs -->
|
||||
## 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.
|
||||
@@ -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 generated by tfplugindocs -->
|
||||
## 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.
|
||||
@@ -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.<scope>.<key>}}. The value is fetched when the deployment runs and is never stored in Dokploy — so rotating it in the vault takes effect on the next deploy with no Terraform change.
|
||||
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.<scope>.<key>}}`. The value is fetched when the deployment runs and is never stored in Dokploy — so rotating it in the vault takes effect on the next deploy with no Terraform change.
|
||||
|
||||
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 generated by tfplugindocs -->
|
||||
## 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`.
|
||||
@@ -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 generated by tfplugindocs -->
|
||||
## 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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -67,6 +67,16 @@ type ResourceSpec struct {
|
||||
// discover the new ID.
|
||||
ListIDs func(ctx context.Context, api *client.Client, model any) (map[string]struct{}, error)
|
||||
|
||||
// CreateDefaults supplies values for API fields that Dokploy's create
|
||||
// schema insists on receiving but that the model cannot produce -- most
|
||||
// often a name the server declines to generate. Only keys missing from
|
||||
// the assembled body are added, so an explicit configuration always wins.
|
||||
CreateDefaults func(model any) map[string]any
|
||||
|
||||
// ConfigValidators are plan-time checks that span several attributes, for
|
||||
// invariants Terraform's schema cannot express on its own.
|
||||
ConfigValidators []resource.ConfigValidator
|
||||
|
||||
// PostRead derives extra model fields from the raw read response, for
|
||||
// values that are not plain top-level columns (for example a project's
|
||||
// default environment, which arrives nested under `environments`).
|
||||
@@ -80,9 +90,10 @@ type genericResource struct {
|
||||
}
|
||||
|
||||
var (
|
||||
_ resource.Resource = &genericResource{}
|
||||
_ resource.ResourceWithConfigure = &genericResource{}
|
||||
_ resource.ResourceWithImportState = &genericResource{}
|
||||
_ resource.Resource = &genericResource{}
|
||||
_ resource.ResourceWithConfigure = &genericResource{}
|
||||
_ resource.ResourceWithImportState = &genericResource{}
|
||||
_ resource.ResourceWithConfigValidators = &genericResource{}
|
||||
)
|
||||
|
||||
func newGenericResource(spec ResourceSpec) func() resource.Resource {
|
||||
@@ -97,6 +108,10 @@ func (r *genericResource) Schema(_ context.Context, _ resource.SchemaRequest, re
|
||||
resp.Schema = r.spec.Schema
|
||||
}
|
||||
|
||||
func (r *genericResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
|
||||
return r.spec.ConfigValidators
|
||||
}
|
||||
|
||||
func (r *genericResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
@@ -125,6 +140,14 @@ func (r *genericResource) Create(ctx context.Context, req resource.CreateRequest
|
||||
return
|
||||
}
|
||||
|
||||
if r.spec.CreateDefaults != nil {
|
||||
for key, value := range r.spec.CreateDefaults(model) {
|
||||
if _, present := body[key]; !present {
|
||||
body[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.spec.NeedsOrganizationID {
|
||||
orgID, err := r.api.OrganizationID(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -44,7 +44,7 @@ func (p *dokployProvider) Metadata(_ context.Context, _ provider.MetadataRequest
|
||||
func (p *dokployProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: "Manage [Dokploy](https://dokploy.com) projects, environments, applications, " +
|
||||
"compose stacks, databases and networking with Terraform.",
|
||||
"compose stacks, databases, Docker networks and networking with Terraform.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"host": schema.StringAttribute{
|
||||
Optional: true,
|
||||
@@ -139,6 +139,7 @@ func (p *dokployProvider) Resources(_ context.Context) []func() resource.Resourc
|
||||
newGenericResource(mariadbResource()),
|
||||
newGenericResource(mongoResource()),
|
||||
newGenericResource(redisResource()),
|
||||
newGenericResource(libsqlResource()),
|
||||
|
||||
newGenericResource(domainResource()),
|
||||
newGenericResource(mountResource()),
|
||||
@@ -146,6 +147,17 @@ func (p *dokployProvider) Resources(_ context.Context) []func() resource.Resourc
|
||||
newGenericResource(redirectResource()),
|
||||
newGenericResource(securityResource()),
|
||||
|
||||
// Docker network management, added in Dokploy v0.30.0.
|
||||
newGenericResource(networkResource()),
|
||||
|
||||
// Scheduling, added in Dokploy v0.30.0.
|
||||
newGenericResource(scheduleResource()),
|
||||
newGenericResource(volumeBackupResource()),
|
||||
|
||||
// External integrations, added in Dokploy v0.30.0.
|
||||
newGenericResource(dnsProviderResource()),
|
||||
newGenericResource(vaultProviderResource()),
|
||||
|
||||
newGenericResource(registryResource()),
|
||||
newGenericResource(sshKeyResource()),
|
||||
newGenericResource(certificateResource()),
|
||||
|
||||
@@ -75,6 +75,7 @@ type applicationModel struct {
|
||||
MemoryLimit types.String `tfsdk:"memory_limit" dokploy:"memoryLimit,nullable"`
|
||||
CPUReserve types.String `tfsdk:"cpu_reservation" dokploy:"cpuReservation,nullable"`
|
||||
CPULimit types.String `tfsdk:"cpu_limit" dokploy:"cpuLimit,nullable"`
|
||||
Icon types.String `tfsdk:"icon" dokploy:"icon,nullable"`
|
||||
Title types.String `tfsdk:"title" dokploy:"title,nullable"`
|
||||
Subtitle types.String `tfsdk:"subtitle" dokploy:"subtitle,nullable"`
|
||||
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled,nullable"`
|
||||
@@ -113,6 +114,7 @@ type applicationModel struct {
|
||||
PreviewCertType types.String `tfsdk:"preview_certificate_type" dokploy:"previewCertificateType,nullable"`
|
||||
PreviewCertResolver types.String `tfsdk:"preview_custom_cert_resolver" dokploy:"previewCustomCertResolver,nullable"`
|
||||
PreviewLimit types.Int64 `tfsdk:"preview_limit" dokploy:"previewLimit,nullable"`
|
||||
PreviewCollabPerms types.Bool `tfsdk:"preview_require_collaborator_permissions" dokploy:"previewRequireCollaboratorPermissions,nullable"`
|
||||
PreviewLabels types.List `tfsdk:"preview_labels" dokploy:"previewLabels,nullable"`
|
||||
|
||||
// Computed
|
||||
@@ -211,6 +213,7 @@ func applicationResource() ResourceSpec {
|
||||
"memory_limit": optionalString("Hard memory limit, for example `512m`."),
|
||||
"cpu_reservation": optionalString("Soft CPU reservation, for example `0.5`."),
|
||||
"cpu_limit": optionalString("Hard CPU limit, for example `1`."),
|
||||
"icon": optionalString("Icon shown next to the service in the Dokploy UI."),
|
||||
"title": optionalString("Display title shown in the Dokploy UI."),
|
||||
"subtitle": optionalString("Display subtitle shown in the Dokploy UI."),
|
||||
"enabled": optionalComputedBool("Whether the application is enabled."),
|
||||
@@ -247,7 +250,9 @@ func applicationResource() ResourceSpec {
|
||||
"preview_certificate_type": enumString("Certificate strategy for preview deployments.", certificateTypes, false),
|
||||
"preview_custom_cert_resolver": optionalString("Traefik certificate resolver for preview deployments."),
|
||||
"preview_limit": optionalComputedInt("Maximum number of concurrent preview deployments."),
|
||||
"preview_labels": optionalComputedStringList("Pull request labels that opt into preview deployments."),
|
||||
"preview_require_collaborator_permissions": optionalComputedBool("Only build previews for pull " +
|
||||
"requests opened by users with repository collaborator permissions."),
|
||||
"preview_labels": optionalComputedStringList("Pull request labels that opt into preview deployments."),
|
||||
|
||||
"application_status": computedString("Current status reported by Dokploy: `idle`, `running`, `done` or `error`."),
|
||||
"created_at": computedString("RFC 3339 timestamp of when the application was created."),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
@@ -19,6 +20,13 @@ type composeModel struct {
|
||||
SourceType types.String `tfsdk:"source_type" dokploy:"sourceType"`
|
||||
Command types.String `tfsdk:"command" dokploy:"command"`
|
||||
Env types.String `tfsdk:"env" dokploy:"env,nullable"`
|
||||
Icon types.String `tfsdk:"icon" dokploy:"icon,nullable"`
|
||||
|
||||
CreateEnvFile types.Bool `tfsdk:"create_env_file" dokploy:"createEnvFile"`
|
||||
|
||||
// serviceNetworks maps a service name in the stack to the networks it
|
||||
// joins, so a stack can attach per-service rather than as a whole.
|
||||
ServiceNetworks jsontypes.Normalized `tfsdk:"service_networks" dokploy:"serviceNetworks,nullable"`
|
||||
|
||||
Repository types.String `tfsdk:"repository" dokploy:"repository,nullable"`
|
||||
Owner types.String `tfsdk:"owner" dokploy:"owner,nullable"`
|
||||
@@ -97,6 +105,11 @@ func composeResource() ResourceSpec {
|
||||
"source_type": enumString("Where the Compose file comes from.", composeSources, false),
|
||||
"command": optionalComputedString("Custom `docker compose` command to run."),
|
||||
"env": optionalString("Environment variables in `KEY=value` format, one per line."),
|
||||
"icon": optionalString("Icon shown next to the stack in the Dokploy UI."),
|
||||
"create_env_file": optionalComputedBool("Write the environment variables to a `.env` file next to " +
|
||||
"the Compose file."),
|
||||
"service_networks": optionalJSON("Per-service Docker network attachments, as a JSON object mapping " +
|
||||
"each service name in the stack to an array of network IDs."),
|
||||
|
||||
"repository": optionalString("GitHub repository name."),
|
||||
"owner": optionalString("GitHub repository owner."),
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
|
||||
"github.com/maxvojtkov/terraform-provider-dokploy/internal/client"
|
||||
)
|
||||
|
||||
// libSQL is the sixth managed database engine, added in Dokploy v0.30.0.
|
||||
//
|
||||
// Its create endpoint is the most awkward in the API: it insists on eleven
|
||||
// keys being present (several only meaningfully null), it will not generate an
|
||||
// appName the way the other engines do, and it returns `true` rather than the
|
||||
// created row. All three are worked around below.
|
||||
|
||||
type libsqlModel struct {
|
||||
ID types.String `tfsdk:"id" dokploy:"libsqlId,id"`
|
||||
Name types.String `tfsdk:"name" dokploy:"name"`
|
||||
AppName types.String `tfsdk:"app_name" dokploy:"appName"`
|
||||
Description types.String `tfsdk:"description" dokploy:"description,nullable"`
|
||||
EnvironmentID types.String `tfsdk:"environment_id" dokploy:"environmentId"`
|
||||
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,nullable"`
|
||||
|
||||
DatabaseUser types.String `tfsdk:"database_user" dokploy:"databaseUser"`
|
||||
DatabasePassword types.String `tfsdk:"database_password" dokploy:"databasePassword"`
|
||||
DockerImage types.String `tfsdk:"docker_image" dokploy:"dockerImage"`
|
||||
|
||||
SqldNode types.String `tfsdk:"sqld_node" dokploy:"sqldNode"`
|
||||
SqldPrimaryURL types.String `tfsdk:"sqld_primary_url" dokploy:"sqldPrimaryUrl,nullable"`
|
||||
EnableNamespaces types.Bool `tfsdk:"enable_namespaces" dokploy:"enableNamespaces"`
|
||||
|
||||
Command types.String `tfsdk:"command" dokploy:"command,nullable"`
|
||||
Env types.String `tfsdk:"env" dokploy:"env,nullable"`
|
||||
MemoryReserve types.String `tfsdk:"memory_reservation" dokploy:"memoryReservation,nullable"`
|
||||
MemoryLimit types.String `tfsdk:"memory_limit" dokploy:"memoryLimit,nullable"`
|
||||
CPUReserve types.String `tfsdk:"cpu_reservation" dokploy:"cpuReservation,nullable"`
|
||||
CPULimit types.String `tfsdk:"cpu_limit" dokploy:"cpuLimit,nullable"`
|
||||
Replicas types.Int64 `tfsdk:"replicas" dokploy:"replicas"`
|
||||
|
||||
ExternalPort types.Int64 `tfsdk:"external_port" dokploy:"externalPort,nullable"`
|
||||
ExternalAdminPort types.Int64 `tfsdk:"external_admin_port" dokploy:"externalAdminPort,nullable"`
|
||||
ExternalGRPCPort types.Int64 `tfsdk:"external_grpc_port" dokploy:"externalGRPCPort,nullable"`
|
||||
|
||||
NetworkIDs types.List `tfsdk:"network_ids" dokploy:"networkIds"`
|
||||
DetachDokployNetwork types.Bool `tfsdk:"detach_dokploy_network" dokploy:"detachDokployNetwork"`
|
||||
|
||||
HealthCheckSwarm jsontypes.Normalized `tfsdk:"health_check_swarm" dokploy:"healthCheckSwarm,nullable"`
|
||||
RestartPolicySwarm jsontypes.Normalized `tfsdk:"restart_policy_swarm" dokploy:"restartPolicySwarm,nullable"`
|
||||
PlacementSwarm jsontypes.Normalized `tfsdk:"placement_swarm" dokploy:"placementSwarm,nullable"`
|
||||
UpdateConfigSwarm jsontypes.Normalized `tfsdk:"update_config_swarm" dokploy:"updateConfigSwarm,nullable"`
|
||||
RollbackConfigSwarm jsontypes.Normalized `tfsdk:"rollback_config_swarm" dokploy:"rollbackConfigSwarm,nullable"`
|
||||
ModeSwarm jsontypes.Normalized `tfsdk:"mode_swarm" dokploy:"modeSwarm,nullable"`
|
||||
LabelsSwarm jsontypes.Normalized `tfsdk:"labels_swarm" dokploy:"labelsSwarm,nullable"`
|
||||
NetworkSwarm jsontypes.Normalized `tfsdk:"network_swarm" dokploy:"networkSwarm,nullable"`
|
||||
EndpointSpecSwarm jsontypes.Normalized `tfsdk:"endpoint_spec_swarm" dokploy:"endpointSpecSwarm,nullable"`
|
||||
StopGracePeriodSwarm types.Int64 `tfsdk:"stop_grace_period_swarm" dokploy:"stopGracePeriodSwarm,nullable"`
|
||||
|
||||
ApplicationStatus types.String `tfsdk:"application_status" dokploy:"applicationStatus,ro"`
|
||||
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
|
||||
}
|
||||
|
||||
func libsqlResource() ResourceSpec {
|
||||
return ResourceSpec{
|
||||
Name: "libsql",
|
||||
UpdateAfterCreate: true,
|
||||
CreateProc: "libsql.create",
|
||||
ReadProc: "libsql.one",
|
||||
UpdateProc: "libsql.update",
|
||||
DeleteProc: "libsql.remove",
|
||||
NewModel: func() any { return &libsqlModel{} },
|
||||
|
||||
// `libsql.create` rejects a body that merely omits a key it considers
|
||||
// required, even when null is the only sensible value, and it refuses
|
||||
// to generate an appName. Fill both in.
|
||||
CreateDefaults: func(model any) map[string]any {
|
||||
libsql, ok := model.(*libsqlModel)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
defaults := map[string]any{
|
||||
"description": nil,
|
||||
"serverId": nil,
|
||||
"sqldPrimaryUrl": nil,
|
||||
}
|
||||
if libsql.AppName.IsNull() || libsql.AppName.IsUnknown() {
|
||||
defaults["appName"] = generateAppName(libsql.Name.ValueString())
|
||||
}
|
||||
return defaults
|
||||
},
|
||||
|
||||
// `libsql.create` returns `true`, so the new ID is found by diffing the
|
||||
// environment's libSQL list.
|
||||
ListIDs: func(ctx context.Context, api *client.Client, model any) (map[string]struct{}, error) {
|
||||
libsql, ok := model.(*libsqlModel)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected *libsqlModel, got %T", model)
|
||||
}
|
||||
raw, err := api.Query(ctx, "environment.one", map[string]any{
|
||||
"environmentId": libsql.EnvironmentID.ValueString(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return collectNestedIDs(raw, "libsql", "libsqlId")
|
||||
},
|
||||
|
||||
Schema: schema.Schema{
|
||||
MarkdownDescription: databaseNote("libSQL") + "\n\n" +
|
||||
"libSQL runs as a `sqld` server. A `primary` node owns the data; a `replica` node follows a " +
|
||||
"primary named by `sqld_primary_url`.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": computedID("Unique libSQL identifier."),
|
||||
"name": requiredString("Display name of the database."),
|
||||
"app_name": optionalComputedReplaceString("Unique Docker service name. Generated from `name` when " +
|
||||
"omitted, because Dokploy's libSQL endpoint does not generate one. Changing it forces a new database."),
|
||||
"description": optionalString("Free-form description."),
|
||||
"environment_id": requiredReplaceString("Environment this database belongs to."),
|
||||
"server_id": optionalReplaceString("Remote server to deploy on. Omit to use the Dokploy host itself."),
|
||||
|
||||
"database_user": requiredString("Database user to create."),
|
||||
"database_password": sensitiveString("Password for the database user.", true),
|
||||
"docker_image": requiredString("libSQL server image to run, for example " +
|
||||
"`ghcr.io/tursodatabase/libsql-server:latest`."),
|
||||
|
||||
"sqld_node": enumStringWithDefault("Role this node plays in a libSQL cluster.", sqldNodes, "primary"),
|
||||
"sqld_primary_url": optionalString("URL of the primary node, when `sqld_node` is `replica`."),
|
||||
"enable_namespaces": optionalComputedBool("Serve multiple logical databases from one instance " +
|
||||
"through libSQL namespaces."),
|
||||
|
||||
"command": optionalString("Override the container entrypoint command."),
|
||||
"env": optionalString("Environment variables in `KEY=value` format, one per line."),
|
||||
"memory_reservation": optionalString("Soft memory reservation, for example `256m`."),
|
||||
"memory_limit": optionalString("Hard memory limit, for example `512m`."),
|
||||
"cpu_reservation": optionalString("Soft CPU reservation, for example `0.5`."),
|
||||
"cpu_limit": optionalString("Hard CPU limit, for example `1`."),
|
||||
"replicas": optionalComputedInt("Number of replicas to run."),
|
||||
|
||||
"external_port": schema.Int64Attribute{Optional: true, MarkdownDescription: "Host port exposing the HTTP API."},
|
||||
"external_admin_port": schema.Int64Attribute{Optional: true, MarkdownDescription: "Host port exposing the admin API."},
|
||||
"external_grpc_port": schema.Int64Attribute{Optional: true, MarkdownDescription: "Host port exposing the gRPC replication endpoint."},
|
||||
|
||||
"network_ids": optionalComputedStringList("IDs of additional Docker networks to attach."),
|
||||
"detach_dokploy_network": optionalComputedBool("Detach the service from the shared `dokploy-network`."),
|
||||
|
||||
"health_check_swarm": optionalJSON("Docker Swarm health check configuration, as a JSON object."),
|
||||
"restart_policy_swarm": optionalJSON("Docker Swarm restart policy, as a JSON object."),
|
||||
"placement_swarm": optionalJSON("Docker Swarm placement constraints, as a JSON object."),
|
||||
"update_config_swarm": optionalJSON("Docker Swarm rolling update configuration, as a JSON object."),
|
||||
"rollback_config_swarm": optionalJSON("Docker Swarm rollback configuration, as a JSON object."),
|
||||
"mode_swarm": optionalJSON("Docker Swarm service mode, as a JSON object."),
|
||||
"labels_swarm": optionalJSON("Docker Swarm service labels, as a JSON object."),
|
||||
"network_swarm": optionalJSON("Docker Swarm network attachments, as a JSON array."),
|
||||
"endpoint_spec_swarm": optionalJSON("Docker Swarm endpoint specification, as a JSON object."),
|
||||
"stop_grace_period_swarm": schema.Int64Attribute{Optional: true, MarkdownDescription: "Grace period in nanoseconds before a container is killed."},
|
||||
|
||||
"application_status": computedString("Current status reported by Dokploy: `idle`, `running`, `done` or `error`."),
|
||||
"created_at": computedString("RFC 3339 timestamp of when the database was created."),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// generateAppName mirrors how Dokploy names a service: a slug of the display
|
||||
// name plus a short random suffix, so two databases called "cache" in
|
||||
// different projects do not collide on the Docker host.
|
||||
func generateAppName(name string) string {
|
||||
var slug strings.Builder
|
||||
lastDash := true
|
||||
for _, r := range strings.ToLower(name) {
|
||||
switch {
|
||||
case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
|
||||
slug.WriteRune(r)
|
||||
lastDash = false
|
||||
case !lastDash:
|
||||
slug.WriteByte('-')
|
||||
lastDash = true
|
||||
}
|
||||
}
|
||||
base := strings.Trim(slug.String(), "-")
|
||||
if base == "" {
|
||||
base = "libsql"
|
||||
}
|
||||
return base + "-" + randomSuffix(6)
|
||||
}
|
||||
|
||||
const suffixAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
func randomSuffix(n int) string {
|
||||
buf := make([]byte, n)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
// crypto/rand does not fail in practice; a fixed suffix still yields a
|
||||
// usable name and Dokploy rejects a genuine collision.
|
||||
return strings.Repeat("0", n)
|
||||
}
|
||||
for i, b := range buf {
|
||||
buf[i] = suffixAlphabet[int(b)%len(suffixAlphabet)]
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package provider_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
)
|
||||
|
||||
const testMountProviderBlock = `
|
||||
provider "dokploy" {
|
||||
host = "https://dokploy.invalid"
|
||||
api_key = "not-used-plan-only"
|
||||
}
|
||||
`
|
||||
|
||||
// A `volume` mount without a volume_name is the dangerous case: Dokploy's
|
||||
// generateVolumeMounts maps a null volumeName to `Source: ""`, which Docker
|
||||
// reads as an anonymous volume. Every deploy then gets a brand-new volume and
|
||||
// the previous one is orphaned, so the data silently does not survive.
|
||||
//
|
||||
// These validations run at plan time, so they need no Dokploy instance.
|
||||
func TestMountValidation(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
config string
|
||||
expectError *regexp.Regexp
|
||||
}{
|
||||
"volume without volume_name": {
|
||||
config: testMountProviderBlock + `
|
||||
resource "dokploy_mount" "test" {
|
||||
type = "volume"
|
||||
mount_path = "/data"
|
||||
service_type = "application"
|
||||
service_id = "app-123"
|
||||
}`,
|
||||
expectError: regexp.MustCompile(`volume_name`),
|
||||
},
|
||||
"volume with empty volume_name": {
|
||||
config: testMountProviderBlock + `
|
||||
resource "dokploy_mount" "test" {
|
||||
type = "volume"
|
||||
mount_path = "/data"
|
||||
volume_name = ""
|
||||
service_type = "application"
|
||||
service_id = "app-123"
|
||||
}`,
|
||||
expectError: regexp.MustCompile(`volume_name`),
|
||||
},
|
||||
"bind without host_path": {
|
||||
config: testMountProviderBlock + `
|
||||
resource "dokploy_mount" "test" {
|
||||
type = "bind"
|
||||
mount_path = "/data"
|
||||
service_type = "application"
|
||||
service_id = "app-123"
|
||||
}`,
|
||||
expectError: regexp.MustCompile(`host_path`),
|
||||
},
|
||||
"file without file_path": {
|
||||
config: testMountProviderBlock + `
|
||||
resource "dokploy_mount" "test" {
|
||||
type = "file"
|
||||
mount_path = "/etc/app"
|
||||
content = "hello"
|
||||
service_type = "application"
|
||||
service_id = "app-123"
|
||||
}`,
|
||||
expectError: regexp.MustCompile(`file_path`),
|
||||
},
|
||||
"volume_name set on a bind mount": {
|
||||
config: testMountProviderBlock + `
|
||||
resource "dokploy_mount" "test" {
|
||||
type = "bind"
|
||||
mount_path = "/data"
|
||||
host_path = "/srv/data"
|
||||
volume_name = "stray"
|
||||
service_type = "application"
|
||||
service_id = "app-123"
|
||||
}`,
|
||||
expectError: regexp.MustCompile(`volume_name`),
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
resource.UnitTest(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: protoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{{
|
||||
Config: tc.config,
|
||||
PlanOnly: true,
|
||||
ExpectError: tc.expectError,
|
||||
}},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()},
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
|
||||
@@ -27,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"`
|
||||
@@ -63,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."),
|
||||
@@ -92,6 +98,43 @@ type mountModel struct {
|
||||
ServiceID types.String `tfsdk:"service_id" dokploy:"serviceId,create"`
|
||||
}
|
||||
|
||||
// mountConfigValidators enforce the type/field pairing that Dokploy itself
|
||||
// does not.
|
||||
//
|
||||
// Dokploy's `generateVolumeMounts` renders a mount as
|
||||
// `{Source: mount.volumeName || "", Target: mount.mountPath}`. A `volume`
|
||||
// mount whose volumeName is null therefore reaches Docker with an empty
|
||||
// source, which Docker treats as an *anonymous* volume: a fresh one is created
|
||||
// on every deploy and the previous one is left orphaned, so the data silently
|
||||
// never survives a redeploy. `mounts.create` accepts the mount regardless, so
|
||||
// nothing surfaces until the data is already gone.
|
||||
//
|
||||
// The same shape applies to `bind` (hostPath) and `file` (filePath).
|
||||
func mountConfigValidators() []resource.ConfigValidator {
|
||||
return []resource.ConfigValidator{
|
||||
&requiredWhen{
|
||||
discriminator: path.Root("type"),
|
||||
value: "volume",
|
||||
attribute: path.Root("volume_name"),
|
||||
rationale: "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.",
|
||||
},
|
||||
&requiredWhen{
|
||||
discriminator: path.Root("type"),
|
||||
value: "bind",
|
||||
attribute: path.Root("host_path"),
|
||||
rationale: "A bind mount with no host path has nothing to bind to.",
|
||||
},
|
||||
&requiredWhen{
|
||||
discriminator: path.Root("type"),
|
||||
value: "file",
|
||||
attribute: path.Root("file_path"),
|
||||
rationale: "Dokploy writes `content` to `file_path` inside the service's files directory.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func mountResource() ResourceSpec {
|
||||
return ResourceSpec{
|
||||
Name: "mount",
|
||||
@@ -100,11 +143,18 @@ func mountResource() ResourceSpec {
|
||||
UpdateProc: "mounts.update",
|
||||
DeleteProc: "mounts.remove",
|
||||
NewModel: func() any { return &mountModel{} },
|
||||
|
||||
ConfigValidators: mountConfigValidators(),
|
||||
|
||||
Schema: schema.Schema{
|
||||
MarkdownDescription: "A volume, bind mount, or config file attached to a Dokploy service.\n\n" +
|
||||
"* `type = \"volume\"` — a named Docker volume; set `volume_name`.\n" +
|
||||
"* `type = \"bind\"` — a path on the host; set `host_path`.\n" +
|
||||
"* `type = \"file\"` — a file rendered from `content`; set `file_path`.",
|
||||
"* `type = \"file\"` — a file rendered from `content`; set `file_path`.\n\n" +
|
||||
"~> **A `volume` mount must set `volume_name`.** Dokploy hands an unset name to Docker as an " +
|
||||
"empty source, which creates a fresh anonymous volume on every deploy and orphans the " +
|
||||
"previous one — the data never survives a redeploy. The provider rejects that combination " +
|
||||
"at plan time.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": computedID("Unique mount identifier."),
|
||||
"type": enumString("The kind of mount to create.", mountTypes, true),
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
// DNS providers and vault providers both arrived in Dokploy v0.30.0. They
|
||||
// share a shape: a name plus a free-form `config` object whose keys depend on
|
||||
// a `providerType` discriminator nested inside it.
|
||||
//
|
||||
// Dokploy masks the credentials in `config` on every read, so the value is
|
||||
// tagged `noread`: the configured value stays authoritative in state instead
|
||||
// of being overwritten with asterisks on the next refresh.
|
||||
|
||||
// ---------------------------------------------------------- DNS provider
|
||||
|
||||
type dnsProviderModel struct {
|
||||
ID types.String `tfsdk:"id" dokploy:"dnsProviderId,id"`
|
||||
Name types.String `tfsdk:"name" dokploy:"name"`
|
||||
Config jsontypes.Normalized `tfsdk:"config" dokploy:"config,noread"`
|
||||
|
||||
ProviderType types.String `tfsdk:"provider_type" dokploy:"providerType,ro"`
|
||||
OrganizationID types.String `tfsdk:"organization_id" dokploy:"organizationId,ro"`
|
||||
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
|
||||
}
|
||||
|
||||
func dnsProviderResource() ResourceSpec {
|
||||
return ResourceSpec{
|
||||
Name: "dns_provider",
|
||||
CreateProc: "dnsProvider.create",
|
||||
ReadProc: "dnsProvider.one",
|
||||
UpdateProc: "dnsProvider.update",
|
||||
DeleteProc: "dnsProvider.remove",
|
||||
NewModel: func() any { return &dnsProviderModel{} },
|
||||
Schema: schema.Schema{
|
||||
MarkdownDescription: "A DNS provider connection Dokploy uses to create records for domains " +
|
||||
"automatically.\n\n" +
|
||||
"`config` is a JSON object whose shape depends on `providerType`:\n\n" +
|
||||
"```hcl\n" +
|
||||
"# Cloudflare\n" +
|
||||
"config = jsonencode({ providerType = \"cloudflare\", apiToken = var.cloudflare_token })\n\n" +
|
||||
"# AWS Route53\n" +
|
||||
"config = jsonencode({\n" +
|
||||
" providerType = \"route53\"\n" +
|
||||
" accessKeyId = var.aws_access_key_id\n" +
|
||||
" secretAccessKey = var.aws_secret_access_key\n" +
|
||||
"})\n" +
|
||||
"```\n\n" +
|
||||
"~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value " +
|
||||
"you configured. Drift in `config` is not detected.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": computedID("Unique DNS provider identifier."),
|
||||
"name": requiredString("Name of the connection. Must be unique within the organization and may " +
|
||||
"contain only letters, digits, `-` and `_`."),
|
||||
"config": schema.StringAttribute{
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
CustomType: jsontypes.NormalizedType{},
|
||||
MarkdownDescription: "Provider credentials as a JSON object, including the `providerType` discriminator.",
|
||||
},
|
||||
"provider_type": computedString("Provider kind derived from `config`: `cloudflare` or `route53`."),
|
||||
"organization_id": computedString("Organization that owns the connection."),
|
||||
"created_at": computedString("RFC 3339 timestamp of when the connection was created."),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------- Vault provider
|
||||
|
||||
type vaultProviderModel struct {
|
||||
ID types.String `tfsdk:"id" dokploy:"vaultProviderId,id"`
|
||||
Name types.String `tfsdk:"name" dokploy:"name"`
|
||||
Config jsontypes.Normalized `tfsdk:"config" dokploy:"config,noread"`
|
||||
Assignments jsontypes.Normalized `tfsdk:"assignments" dokploy:"assignments"`
|
||||
|
||||
ProviderType types.String `tfsdk:"provider_type" dokploy:"providerType,ro"`
|
||||
OrganizationID types.String `tfsdk:"organization_id" dokploy:"organizationId,ro"`
|
||||
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
|
||||
}
|
||||
|
||||
func vaultProviderResource() ResourceSpec {
|
||||
return ResourceSpec{
|
||||
Name: "vault_provider",
|
||||
CreateProc: "vaultProvider.create",
|
||||
ReadProc: "vaultProvider.one",
|
||||
UpdateProc: "vaultProvider.update",
|
||||
DeleteProc: "vaultProvider.remove",
|
||||
NewModel: func() any { return &vaultProviderModel{} },
|
||||
Schema: schema.Schema{
|
||||
MarkdownDescription: "An external secret manager Dokploy resolves environment variables from at " +
|
||||
"deploy time.\n\n" +
|
||||
"Reference a secret from any `env` value with `${{vault.<scope>.<key>}}`. The value is fetched " +
|
||||
"when the deployment runs and is never stored in Dokploy — so rotating it in the vault takes " +
|
||||
"effect on the next deploy with no Terraform change.\n\n" +
|
||||
"Supported `providerType` values: `hashicorp` (Vault/OpenBao), `infisical`, `aws` " +
|
||||
"(Secrets Manager), `doppler`, `azure` (Key Vault) and `scaleway`.\n\n" +
|
||||
"```hcl\n" +
|
||||
"config = jsonencode({\n" +
|
||||
" providerType = \"hashicorp\"\n" +
|
||||
" url = \"https://vault.example.com\"\n" +
|
||||
" token = var.vault_token\n" +
|
||||
" mount = \"secret\"\n" +
|
||||
"})\n" +
|
||||
"```\n\n" +
|
||||
"~> Dokploy masks the credentials when reading a provider back, so Terraform keeps the value " +
|
||||
"you configured. Drift in `config` is not detected.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": computedID("Unique vault provider identifier."),
|
||||
"name": requiredString("Name of the connection, unique within the organization."),
|
||||
"config": schema.StringAttribute{
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
CustomType: jsontypes.NormalizedType{},
|
||||
MarkdownDescription: "Provider credentials as a JSON object, including the `providerType` discriminator.",
|
||||
},
|
||||
"assignments": schema.StringAttribute{
|
||||
Required: true,
|
||||
CustomType: jsontypes.NormalizedType{},
|
||||
MarkdownDescription: "JSON array scoping which projects or environments may resolve secrets " +
|
||||
"from this provider. Pass `jsonencode([])` to leave it unscoped.",
|
||||
},
|
||||
"provider_type": computedString("Provider kind derived from `config`."),
|
||||
"organization_id": computedString("Organization that owns the connection."),
|
||||
"created_at": computedString("RFC 3339 timestamp of when the connection was created."),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------- Schedule
|
||||
|
||||
type scheduleModel struct {
|
||||
ID types.String `tfsdk:"id" dokploy:"scheduleId,id"`
|
||||
Name types.String `tfsdk:"name" dokploy:"name"`
|
||||
Description types.String `tfsdk:"description" dokploy:"description,nullable"`
|
||||
CronExpression types.String `tfsdk:"cron_expression" dokploy:"cronExpression"`
|
||||
Command types.String `tfsdk:"command" dokploy:"command"`
|
||||
Script types.String `tfsdk:"script" dokploy:"script,nullable"`
|
||||
ShellType types.String `tfsdk:"shell_type" dokploy:"shellType"`
|
||||
ScheduleType types.String `tfsdk:"schedule_type" dokploy:"scheduleType"`
|
||||
Timezone types.String `tfsdk:"timezone" dokploy:"timezone,nullable"`
|
||||
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled"`
|
||||
|
||||
AppName types.String `tfsdk:"app_name" dokploy:"appName,nullable"`
|
||||
ServiceName types.String `tfsdk:"service_name" dokploy:"serviceName,nullable"`
|
||||
|
||||
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,nullable"`
|
||||
ComposeID types.String `tfsdk:"compose_id" dokploy:"composeId,nullable"`
|
||||
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,nullable"`
|
||||
|
||||
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
|
||||
}
|
||||
|
||||
func scheduleResource() ResourceSpec {
|
||||
return ResourceSpec{
|
||||
Name: "schedule",
|
||||
CreateProc: "schedule.create",
|
||||
ReadProc: "schedule.one",
|
||||
UpdateProc: "schedule.update",
|
||||
DeleteProc: "schedule.delete",
|
||||
NewModel: func() any { return &scheduleModel{} },
|
||||
Schema: schema.Schema{
|
||||
MarkdownDescription: "A cron job Dokploy runs on a schedule.\n\n" +
|
||||
"`schedule_type` selects where the command runs:\n\n" +
|
||||
"* `application` — inside a running application container; set `application_id`.\n" +
|
||||
"* `compose` — inside one service of a Compose stack; set `compose_id` and `service_name`.\n" +
|
||||
"* `server` — on a remote server; set `server_id`.\n" +
|
||||
"* `dokploy-server` — on the Dokploy host itself.\n\n" +
|
||||
"~> A schedule targeting an application runs inside its container, so the container has to be " +
|
||||
"running when the cron fires.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": computedID("Unique schedule identifier."),
|
||||
"name": requiredString("Display name of the schedule."),
|
||||
"description": optionalString("Free-form description."),
|
||||
"cron_expression": requiredString("Standard five-field cron expression, for example `0 3 * * *`."),
|
||||
"command": requiredString("Command to run."),
|
||||
"script": optionalString("Multi-line script to run instead of a single command."),
|
||||
"shell_type": enumStringWithDefault("Shell used to interpret the command.", shellTypes, "bash"),
|
||||
"schedule_type": enumStringWithDefault("Where the command runs.", scheduleTypes, "application"),
|
||||
"timezone": optionalString("IANA timezone the cron expression is evaluated in, for example `Europe/Berlin`."),
|
||||
"enabled": optionalComputedBool("Whether the schedule is active."),
|
||||
|
||||
"app_name": optionalComputedString("Docker service name the schedule targets. Derived by Dokploy when omitted."),
|
||||
"service_name": optionalString("Service inside a Compose stack to run the command in."),
|
||||
|
||||
"application_id": optionalReplaceString("Application this schedule belongs to."),
|
||||
"compose_id": optionalReplaceString("Compose stack this schedule belongs to."),
|
||||
"server_id": optionalReplaceString("Server this schedule runs on."),
|
||||
|
||||
"created_at": computedString("RFC 3339 timestamp of when the schedule was created."),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------- Volume backup
|
||||
|
||||
type volumeBackupModel struct {
|
||||
ID types.String `tfsdk:"id" dokploy:"volumeBackupId,id"`
|
||||
Name types.String `tfsdk:"name" dokploy:"name"`
|
||||
VolumeName types.String `tfsdk:"volume_name" dokploy:"volumeName"`
|
||||
Prefix types.String `tfsdk:"prefix" dokploy:"prefix"`
|
||||
CronExpression types.String `tfsdk:"cron_expression" dokploy:"cronExpression"`
|
||||
DestinationID types.String `tfsdk:"destination_id" dokploy:"destinationId"`
|
||||
|
||||
ServiceType types.String `tfsdk:"service_type" dokploy:"serviceType"`
|
||||
AppName types.String `tfsdk:"app_name" dokploy:"appName,nullable"`
|
||||
ServiceName types.String `tfsdk:"service_name" dokploy:"serviceName,nullable"`
|
||||
TurnOff types.Bool `tfsdk:"turn_off" dokploy:"turnOff"`
|
||||
KeepLatestCount types.Int64 `tfsdk:"keep_latest_count" dokploy:"keepLatestCount,nullable"`
|
||||
Enabled types.Bool `tfsdk:"enabled" dokploy:"enabled"`
|
||||
|
||||
ApplicationID types.String `tfsdk:"application_id" dokploy:"applicationId,nullable"`
|
||||
ComposeID types.String `tfsdk:"compose_id" dokploy:"composeId,nullable"`
|
||||
PostgresID types.String `tfsdk:"postgres_id" dokploy:"postgresId,nullable"`
|
||||
MySQLID types.String `tfsdk:"mysql_id" dokploy:"mysqlId,nullable"`
|
||||
MariaDBID types.String `tfsdk:"mariadb_id" dokploy:"mariadbId,nullable"`
|
||||
MongoID types.String `tfsdk:"mongo_id" dokploy:"mongoId,nullable"`
|
||||
RedisID types.String `tfsdk:"redis_id" dokploy:"redisId,nullable"`
|
||||
LibsqlID types.String `tfsdk:"libsql_id" dokploy:"libsqlId,nullable"`
|
||||
|
||||
CreatedAt types.String `tfsdk:"created_at" dokploy:"createdAt,ro"`
|
||||
}
|
||||
|
||||
func volumeBackupResource() ResourceSpec {
|
||||
return ResourceSpec{
|
||||
Name: "volume_backup",
|
||||
CreateProc: "volumeBackups.create",
|
||||
ReadProc: "volumeBackups.one",
|
||||
UpdateProc: "volumeBackups.update",
|
||||
DeleteProc: "volumeBackups.delete",
|
||||
NewModel: func() any { return &volumeBackupModel{} },
|
||||
Schema: schema.Schema{
|
||||
MarkdownDescription: "A scheduled backup of a Docker volume to a configured " +
|
||||
"`dokploy_destination`.\n\n" +
|
||||
"This is the counterpart to a `dokploy_mount` with `type = \"volume\"`: the mount gives the " +
|
||||
"volume a stable name, and this resource copies its contents off the host on a schedule.\n\n" +
|
||||
"Set exactly one of the `*_id` attributes to say which service owns the volume.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": computedID("Unique volume backup identifier."),
|
||||
"name": requiredString("Display name of the backup job."),
|
||||
"volume_name": requiredString("Name of the Docker volume to back up."),
|
||||
"prefix": requiredString("Path prefix inside the destination bucket, for example `backups/shop/`."),
|
||||
"cron_expression": requiredString("Standard five-field cron expression, for example `0 4 * * *`."),
|
||||
"destination_id": requiredString("Backup destination (S3-compatible bucket) to upload to."),
|
||||
|
||||
"service_type": enumStringWithDefault("The kind of service that owns the volume.", volumeBackupServiceTypes, "application"),
|
||||
"app_name": optionalComputedString("Docker service name that owns the volume. Derived by Dokploy when omitted."),
|
||||
"service_name": optionalString("Service inside a Compose stack that owns the volume."),
|
||||
"turn_off": optionalComputedBool("Stop the service while the backup runs. Slower, but guarantees a " +
|
||||
"consistent copy of data that is being written to."),
|
||||
"keep_latest_count": optionalComputedInt("Number of backups to retain. Older ones are pruned."),
|
||||
"enabled": optionalComputedBool("Whether the backup schedule is active."),
|
||||
|
||||
"application_id": optionalReplaceString("Application that owns the volume."),
|
||||
"compose_id": optionalReplaceString("Compose stack that owns the volume."),
|
||||
"postgres_id": optionalReplaceString("PostgreSQL instance that owns the volume."),
|
||||
"mysql_id": optionalReplaceString("MySQL instance that owns the volume."),
|
||||
"mariadb_id": optionalReplaceString("MariaDB instance that owns the volume."),
|
||||
"mongo_id": optionalReplaceString("MongoDB instance that owns the volume."),
|
||||
"redis_id": optionalReplaceString("Redis instance that owns the volume."),
|
||||
"libsql_id": optionalReplaceString("libSQL instance that owns the volume."),
|
||||
|
||||
"created_at": computedString("RFC 3339 timestamp of when the backup job was created."),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,12 @@ func optionalJSON(description string) schema.StringAttribute {
|
||||
}
|
||||
}
|
||||
|
||||
// enumValidator is the validator list for a string constrained to a fixed set,
|
||||
// for attributes assembled by hand rather than through enumString.
|
||||
func enumValidator(values []string) []validator.String {
|
||||
return []validator.String{stringvalidator.OneOf(values...)}
|
||||
}
|
||||
|
||||
func joinBackticked(values []string) string {
|
||||
out := ""
|
||||
for i, v := range values {
|
||||
@@ -217,7 +223,18 @@ var (
|
||||
composeSources = []string{"git", "github", "gitlab", "bitbucket", "gitea", "raw"}
|
||||
domainTypes = []string{"compose", "application", "preview"}
|
||||
mountTypes = []string{"bind", "volume", "file"}
|
||||
serviceTypes = []string{"application", "postgres", "mysql", "mariadb", "mongo", "redis", "compose"}
|
||||
serviceTypes = []string{"application", "postgres", "mysql", "mariadb", "mongo", "redis", "compose", "libsql"}
|
||||
protocolTypes = []string{"tcp", "udp"}
|
||||
publishModes = []string{"ingress", "host"}
|
||||
|
||||
// Added in Dokploy v0.30.0.
|
||||
networkDrivers = []string{"bridge", "overlay"}
|
||||
shellTypes = []string{"bash", "sh"}
|
||||
scheduleTypes = []string{"application", "compose", "server", "dokploy-server"}
|
||||
sqldNodes = []string{"primary", "replica"}
|
||||
|
||||
// volumeBackups accepts the service types plus libsql.
|
||||
volumeBackupServiceTypes = []string{
|
||||
"application", "postgres", "mysql", "mariadb", "mongo", "redis", "compose", "libsql",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
// requiredWhen declares that `attribute` must hold a non-empty value whenever
|
||||
// `discriminator` equals `value`, and must be absent otherwise.
|
||||
//
|
||||
// Terraform's schema language cannot express "required, but only for this
|
||||
// variant", and Dokploy's Zod schemas accept every combination -- so without a
|
||||
// provider-side check a nonsensical resource is created without complaint. The
|
||||
// `dokploy_mount` case is the reason this exists: see mountConfigValidators.
|
||||
type requiredWhen struct {
|
||||
discriminator path.Path
|
||||
value string
|
||||
attribute path.Path
|
||||
// rationale explains the consequence of getting it wrong, so the error
|
||||
// tells the practitioner why rather than only what.
|
||||
rationale string
|
||||
}
|
||||
|
||||
var _ resource.ConfigValidator = &requiredWhen{}
|
||||
|
||||
func (v *requiredWhen) Description(ctx context.Context) string {
|
||||
return v.MarkdownDescription(ctx)
|
||||
}
|
||||
|
||||
func (v *requiredWhen) MarkdownDescription(_ context.Context) string {
|
||||
return fmt.Sprintf("`%s` is required when `%s` is `%s`, and must not be set otherwise.",
|
||||
v.attribute, v.discriminator, v.value)
|
||||
}
|
||||
|
||||
func (v *requiredWhen) ValidateResource(
|
||||
ctx context.Context,
|
||||
req resource.ValidateConfigRequest,
|
||||
resp *resource.ValidateConfigResponse,
|
||||
) {
|
||||
var discriminator types.String
|
||||
resp.Diagnostics.Append(req.Config.GetAttribute(ctx, v.discriminator, &discriminator)...)
|
||||
if resp.Diagnostics.HasError() || discriminator.IsNull() || discriminator.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
var attribute types.String
|
||||
resp.Diagnostics.Append(req.Config.GetAttribute(ctx, v.attribute, &attribute)...)
|
||||
if resp.Diagnostics.HasError() || attribute.IsUnknown() {
|
||||
// An unknown value cannot be checked at plan time; it is resolved
|
||||
// during apply and Dokploy validates it there.
|
||||
return
|
||||
}
|
||||
|
||||
matches := discriminator.ValueString() == v.value
|
||||
empty := attribute.IsNull() || attribute.ValueString() == ""
|
||||
|
||||
switch {
|
||||
case matches && empty:
|
||||
detail := fmt.Sprintf("`%s` must be set to a non-empty value when `%s` is `%s`.",
|
||||
v.attribute, v.discriminator, v.value)
|
||||
if v.rationale != "" {
|
||||
detail += "\n\n" + v.rationale
|
||||
}
|
||||
resp.Diagnostics.AddAttributeError(v.attribute,
|
||||
fmt.Sprintf("Missing %s", v.attribute), detail)
|
||||
|
||||
case !matches && !empty:
|
||||
resp.Diagnostics.AddAttributeError(v.attribute,
|
||||
fmt.Sprintf("Unexpected %s", v.attribute),
|
||||
fmt.Sprintf("`%s` only applies when `%s` is `%s`, but it is `%s`. "+
|
||||
"Dokploy ignores the value, so leaving it set hides a mistake.",
|
||||
v.attribute, v.discriminator, v.value, discriminator.ValueString()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user