A `dokploy_mount` with `type = "volume"` and no `volume_name` was accepted
by both this provider and Dokploy. Dokploy renders the mount as
`{Source: volumeName || "", Target: mountPath}`, and Docker reads an empty
source as an anonymous volume: every deploy created a fresh one and orphaned
the last, so the data never survived a redeploy while disk usage climbed.
Nothing errored at any point, which is what made it worth catching here.
The pairing is now checked at plan time, before anything is created, and the
error explains the consequence rather than only the rule. The same validator
covers `bind` without `host_path` and `file` without `file_path`, and rejects
a field set against the wrong type, which Dokploy would otherwise ignore.
Verified against a live v0.30.2 instance: the offending config plans cleanly
before the change and is refused after it.
95 lines
2.5 KiB
Go
95 lines
2.5 KiB
Go
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,
|
|
}},
|
|
})
|
|
})
|
|
}
|
|
}
|