A Terraform provider for Dokploy

Plugin-framework provider covering projects, environments, applications,
Compose stacks, managed databases, domains, mounts, ports, redirects,
basic auth, registries, SSH keys, certificates and backup destinations,
over Dokploy's tRPC-over-REST API.

The shim package exposes the provider to other Go modules, which is how
pulumi-dokploy bridges it.
This commit is contained in:
2026-08-09 12:17:26 +03:00
commit a6d8aa8b52
160 changed files with 24260 additions and 0 deletions

View File

@@ -0,0 +1,293 @@
package provider_test
import (
"fmt"
"os"
"testing"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/maxvojtkov/terraform-provider-dokploy/internal/provider"
)
// Acceptance tests talk to a real Dokploy instance. They create resources with
// a `tfacc-` prefix and destroy them again, so point them at a scratch
// instance rather than production:
//
// TF_ACC=1 \
// DOKPLOY_HOST=https://dokploy.example.com \
// DOKPLOY_API_KEY=... \
// go test ./internal/provider/ -v -timeout 30m
var protoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
"dokploy": providerserver.NewProtocol6WithError(provider.New("test")()),
}
func testAccPreCheck(t *testing.T) {
t.Helper()
for _, key := range []string{"DOKPLOY_HOST", "DOKPLOY_API_KEY"} {
if os.Getenv(key) == "" {
t.Fatalf("%s must be set for acceptance tests", key)
}
}
}
func TestAccProject(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: protoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "dokploy_project" "test" {
name = "tfacc-project"
description = "created by acceptance tests"
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("dokploy_project.test", "name", "tfacc-project"),
resource.TestCheckResourceAttr("dokploy_project.test", "description", "created by acceptance tests"),
resource.TestCheckResourceAttrSet("dokploy_project.test", "id"),
// Dokploy creates a default environment with every project.
resource.TestCheckResourceAttrSet("dokploy_project.test", "default_environment_id"),
resource.TestCheckResourceAttrSet("dokploy_project.test", "organization_id"),
),
},
{
ResourceName: "dokploy_project.test",
ImportState: true,
ImportStateVerify: true,
},
{
Config: `
resource "dokploy_project" "test" {
name = "tfacc-project-renamed"
description = "updated"
env = "SHARED=1"
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("dokploy_project.test", "name", "tfacc-project-renamed"),
resource.TestCheckResourceAttr("dokploy_project.test", "description", "updated"),
resource.TestCheckResourceAttr("dokploy_project.test", "env", "SHARED=1"),
),
},
},
})
}
func TestAccEnvironment(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: protoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "dokploy_project" "test" {
name = "tfacc-env-project"
}
resource "dokploy_environment" "test" {
name = "tfacc-staging"
project_id = dokploy_project.test.id
env = "STAGE=1"
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("dokploy_environment.test", "name", "tfacc-staging"),
// env is only writable through a follow-up update; assert it stuck.
resource.TestCheckResourceAttr("dokploy_environment.test", "env", "STAGE=1"),
resource.TestCheckResourceAttr("dokploy_environment.test", "is_default", "false"),
),
},
},
})
}
// Applications are created with a minimal payload and then configured through
// application.update, so this asserts that update-only fields survive create.
func TestAccApplicationDockerSource(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: protoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccApplicationConfig("512m", 1),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("dokploy_application.test", "source_type", "docker"),
resource.TestCheckResourceAttr("dokploy_application.test", "docker_image", "traefik/whoami:latest"),
resource.TestCheckResourceAttr("dokploy_application.test", "memory_limit", "512m"),
resource.TestCheckResourceAttr("dokploy_application.test", "replicas", "1"),
resource.TestCheckResourceAttr("dokploy_application.test", "env", "GREETING=hello"),
resource.TestCheckResourceAttrSet("dokploy_application.test", "app_name"),
),
},
{
ResourceName: "dokploy_application.test",
ImportState: true,
ImportStateVerify: true,
},
{
// An in-place update must not force replacement.
Config: testAccApplicationConfig("1g", 3),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("dokploy_application.test", "memory_limit", "1g"),
resource.TestCheckResourceAttr("dokploy_application.test", "replicas", "3"),
),
},
},
})
}
func testAccApplicationConfig(memoryLimit string, replicas int) string {
return fmt.Sprintf(`
resource "dokploy_project" "test" {
name = "tfacc-app-project"
}
resource "dokploy_application" "test" {
name = "tfacc-app"
environment_id = dokploy_project.test.default_environment_id
source_type = "docker"
docker_image = "traefik/whoami:latest"
env = "GREETING=hello"
memory_limit = %q
replicas = %d
}`, memoryLimit, replicas)
}
func TestAccComposeAndDomain(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: protoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "dokploy_project" "test" {
name = "tfacc-compose-project"
}
resource "dokploy_compose" "test" {
name = "tfacc-stack"
environment_id = dokploy_project.test.default_environment_id
compose_type = "docker-compose"
source_type = "raw"
compose_file = <<-YAML
services:
whoami:
image: traefik/whoami:latest
YAML
}
resource "dokploy_domain" "test" {
compose_id = dokploy_compose.test.id
domain_type = "compose"
service_name = "whoami"
host = "tfacc.example.invalid"
port = 80
https = false
certificate_type = "none"
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("dokploy_compose.test", "compose_type", "docker-compose"),
resource.TestCheckResourceAttrSet("dokploy_compose.test", "app_name"),
resource.TestCheckResourceAttr("dokploy_domain.test", "host", "tfacc.example.invalid"),
resource.TestCheckResourceAttr("dokploy_domain.test", "service_name", "whoami"),
),
},
},
})
}
// Databases follow the same create-then-update path as applications.
func TestAccPostgres(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: protoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "dokploy_project" "test" {
name = "tfacc-pg-project"
}
resource "dokploy_postgres" "test" {
name = "tfacc-pg"
environment_id = dokploy_project.test.default_environment_id
docker_image = "postgres:16-alpine"
database_name = "acc"
database_user = "acc"
database_password = "acc-password"
memory_limit = "512m"
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("dokploy_postgres.test", "database_name", "acc"),
// memory_limit is rejected by postgres.create and must be
// written by the follow-up update.
resource.TestCheckResourceAttr("dokploy_postgres.test", "memory_limit", "512m"),
),
},
},
})
}
// redirects.create returns `true` rather than a row, so the provider finds the
// new ID by diffing the application's redirect list.
func TestAccRedirectIDDiscovery(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: protoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "dokploy_project" "test" {
name = "tfacc-redirect-project"
}
resource "dokploy_application" "test" {
name = "tfacc-redirect-app"
environment_id = dokploy_project.test.default_environment_id
source_type = "docker"
docker_image = "traefik/whoami:latest"
}
resource "dokploy_redirect" "test" {
application_id = dokploy_application.test.id
regex = "^https://old\\.(.*)"
replacement = "https://new.$${1}"
permanent = true
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("dokploy_redirect.test", "id"),
resource.TestCheckResourceAttr("dokploy_redirect.test", "permanent", "true"),
),
},
},
})
}
func TestAccProjectsDataSource(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: protoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: `
resource "dokploy_project" "test" {
name = "tfacc-ds-project"
}
data "dokploy_project" "test" {
id = dokploy_project.test.id
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.dokploy_project.test", "name", "tfacc-ds-project"),
resource.TestCheckResourceAttrSet("data.dokploy_project.test", "default_environment_id"),
resource.TestCheckResourceAttr("data.dokploy_project.test", "environments.#", "1"),
),
},
},
})
}