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.
79 lines
2.7 KiB
Go
79 lines
2.7 KiB
Go
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()))
|
|
}
|
|
}
|