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,134 @@
package tfmap
import (
"encoding/json"
"testing"
"github.com/hashicorp/terraform-plugin-framework/types"
)
type sample struct {
ID types.String `tfsdk:"id" dokploy:"thingId,id"`
Name types.String `tfsdk:"name" dokploy:"name"`
Desc types.String `tfsdk:"description" dokploy:"description,nullable"`
Networks types.List `tfsdk:"network_ids" dokploy:"networkIds"`
Detach types.Bool `tfsdk:"detach" dokploy:"detach"`
Replicas types.Int64 `tfsdk:"replicas" dokploy:"replicas"`
Secret types.String `tfsdk:"secret" dokploy:"secret,noread"`
ServerID types.String `tfsdk:"server_id" dokploy:"serverId,create"`
Status types.String `tfsdk:"status" dokploy:"status,ro"`
}
// A JSON null in the response must produce a null attribute, never leave the
// attribute unknown: Terraform rejects unknown values after apply.
func TestFromAPIConvertsJSONNullToNullAttribute(t *testing.T) {
model := &sample{
Networks: types.ListUnknown(types.StringType),
Detach: types.BoolUnknown(),
Replicas: types.Int64Unknown(),
}
raw := json.RawMessage(`{"networkIds":null,"detach":null,"replicas":null}`)
if err := FromAPI(raw, model); err != nil {
t.Fatalf("FromAPI returned an error: %v", err)
}
if model.Networks.IsUnknown() {
t.Error("networkIds stayed unknown; want null")
}
if !model.Networks.IsNull() {
t.Errorf("networkIds = %v; want null", model.Networks)
}
if model.Detach.IsUnknown() || !model.Detach.IsNull() {
t.Errorf("detach = %v; want null", model.Detach)
}
if model.Replicas.IsUnknown() || !model.Replicas.IsNull() {
t.Errorf("replicas = %v; want null", model.Replicas)
}
}
// Keys absent from the response must not clobber existing values, but they
// must also not survive as unknown once NullifyUnknown runs.
func TestFromAPILeavesAbsentKeysUntouched(t *testing.T) {
model := &sample{
Name: types.StringValue("keep-me"),
Networks: types.ListUnknown(types.StringType),
}
if err := FromAPI(json.RawMessage(`{"description":"hi"}`), model); err != nil {
t.Fatalf("FromAPI returned an error: %v", err)
}
if got := model.Name.ValueString(); got != "keep-me" {
t.Errorf("name = %q; want %q", got, "keep-me")
}
if got := model.Desc.ValueString(); got != "hi" {
t.Errorf("description = %q; want %q", got, "hi")
}
if !model.Networks.IsUnknown() {
t.Error("networkIds should still be unknown before NullifyUnknown runs")
}
if err := NullifyUnknown(model); err != nil {
t.Fatalf("NullifyUnknown returned an error: %v", err)
}
if model.Networks.IsUnknown() {
t.Error("networkIds stayed unknown after NullifyUnknown")
}
}
// noread fields must never be overwritten by the API response.
func TestFromAPISkipsNoReadFields(t *testing.T) {
model := &sample{Secret: types.StringValue("plaintext")}
if err := FromAPI(json.RawMessage(`{"secret":"$2b$10$hashed"}`), model); err != nil {
t.Fatalf("FromAPI returned an error: %v", err)
}
if got := model.Secret.ValueString(); got != "plaintext" {
t.Errorf("secret = %q; want the configured value to survive", got)
}
}
func TestToAPIPhaseSelection(t *testing.T) {
model := &sample{
ID: types.StringValue("abc123"),
Name: types.StringValue("thing"),
Desc: types.StringNull(),
ServerID: types.StringValue("srv1"),
Status: types.StringValue("running"),
Networks: types.ListUnknown(types.StringType),
}
create, err := ToAPI(model, PhaseCreate)
if err != nil {
t.Fatalf("ToAPI(create) returned an error: %v", err)
}
if _, ok := create["thingId"]; ok {
t.Error("create body must not carry the server-generated ID")
}
if create["serverId"] != "srv1" {
t.Errorf("create body serverId = %v; want srv1", create["serverId"])
}
if _, ok := create["status"]; ok {
t.Error("read-only fields must never be sent")
}
if _, ok := create["networkIds"]; ok {
t.Error("unknown values must never be sent")
}
// description is null but tagged nullable, so it is sent explicitly.
value, ok := create["description"]
if !ok || value != nil {
t.Errorf("description = %v (present %v); want an explicit null", value, ok)
}
update, err := ToAPI(model, PhaseUpdate)
if err != nil {
t.Fatalf("ToAPI(update) returned an error: %v", err)
}
if update["thingId"] != "abc123" {
t.Errorf("update body thingId = %v; want abc123", update["thingId"])
}
if _, ok := update["serverId"]; ok {
t.Error("create-only fields must not be sent on update")
}
}