Files
Max Vojtkov a6d8aa8b52 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.
2026-08-09 12:17:26 +03:00

468 lines
17 KiB
Go

package provider
import (
"context"
"encoding/json"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/maxvojtkov/terraform-provider-dokploy/internal/client"
)
// baseDataSource wires the shared API client into each data source.
type baseDataSource struct {
api *client.Client
}
func (d *baseDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
api, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected provider data",
fmt.Sprintf("Expected *client.Client, got %T. This is a bug in the provider.", req.ProviderData),
)
return
}
d.api = api
}
// ------------------------------------------------------------ dokploy_project
type projectDataSourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Description types.String `tfsdk:"description"`
Env types.String `tfsdk:"env"`
CreatedAt types.String `tfsdk:"created_at"`
OrganizationID types.String `tfsdk:"organization_id"`
DefaultEnvironmentID types.String `tfsdk:"default_environment_id"`
Environments types.List `tfsdk:"environments"`
}
type projectDataSource struct{ baseDataSource }
func newProjectDataSource() datasource.DataSource { return &projectDataSource{} }
func (d *projectDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_project"
}
var environmentObjectType = types.ObjectType{AttrTypes: map[string]attr.Type{
"id": types.StringType,
"name": types.StringType,
"is_default": types.BoolType,
}}
func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Look up an existing Dokploy project by ID, including its environments.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{Required: true, MarkdownDescription: "Project identifier."},
"name": schema.StringAttribute{Computed: true, MarkdownDescription: "Project name."},
"description": schema.StringAttribute{Computed: true, MarkdownDescription: "Project description."},
"env": schema.StringAttribute{Computed: true, MarkdownDescription: "Project-wide environment variables."},
"created_at": schema.StringAttribute{Computed: true, MarkdownDescription: "Creation timestamp."},
"organization_id": schema.StringAttribute{Computed: true, MarkdownDescription: "Owning organization."},
"default_environment_id": schema.StringAttribute{
Computed: true,
MarkdownDescription: "ID of the project's default environment.",
},
"environments": schema.ListAttribute{
Computed: true,
ElementType: environmentObjectType,
MarkdownDescription: "Every environment in the project.",
},
},
}
}
func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config projectDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
raw, err := d.api.Query(ctx, "project.one", map[string]any{"projectId": config.ID.ValueString()})
if err != nil {
resp.Diagnostics.AddError("Failed to read project", err.Error())
return
}
var payload struct {
ProjectID string `json:"projectId"`
Name string `json:"name"`
Description *string `json:"description"`
Env string `json:"env"`
CreatedAt string `json:"createdAt"`
OrganizationID string `json:"organizationId"`
Environments []struct {
EnvironmentID string `json:"environmentId"`
Name string `json:"name"`
IsDefault bool `json:"isDefault"`
} `json:"environments"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
resp.Diagnostics.AddError("Failed to decode project", err.Error())
return
}
config.Name = types.StringValue(payload.Name)
config.Description = optionalStringValue(payload.Description)
config.Env = types.StringValue(payload.Env)
config.CreatedAt = types.StringValue(payload.CreatedAt)
config.OrganizationID = types.StringValue(payload.OrganizationID)
config.DefaultEnvironmentID = types.StringNull()
elements := make([]attr.Value, 0, len(payload.Environments))
for _, env := range payload.Environments {
if env.IsDefault {
config.DefaultEnvironmentID = types.StringValue(env.EnvironmentID)
}
object, diags := types.ObjectValue(environmentObjectType.AttrTypes, map[string]attr.Value{
"id": types.StringValue(env.EnvironmentID),
"name": types.StringValue(env.Name),
"is_default": types.BoolValue(env.IsDefault),
})
resp.Diagnostics.Append(diags...)
elements = append(elements, object)
}
if resp.Diagnostics.HasError() {
return
}
list, diags := types.ListValue(environmentObjectType, elements)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
config.Environments = list
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...)
}
// ----------------------------------------------------------- dokploy_projects
type projectsDataSourceModel struct {
Projects types.List `tfsdk:"projects"`
}
type projectsDataSource struct{ baseDataSource }
func newProjectsDataSource() datasource.DataSource { return &projectsDataSource{} }
func (d *projectsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_projects"
}
var projectSummaryType = types.ObjectType{AttrTypes: map[string]attr.Type{
"id": types.StringType,
"name": types.StringType,
"description": types.StringType,
"default_environment_id": types.StringType,
}}
func (d *projectsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "List every Dokploy project the API token can see.",
Attributes: map[string]schema.Attribute{
"projects": schema.ListAttribute{
Computed: true,
ElementType: projectSummaryType,
MarkdownDescription: "All visible projects.",
},
},
}
}
func (d *projectsDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) {
raw, err := d.api.Query(ctx, "project.all", nil)
if err != nil {
resp.Diagnostics.AddError("Failed to list projects", err.Error())
return
}
var payload []struct {
ProjectID string `json:"projectId"`
Name string `json:"name"`
Description *string `json:"description"`
Environments []struct {
EnvironmentID string `json:"environmentId"`
IsDefault bool `json:"isDefault"`
} `json:"environments"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
resp.Diagnostics.AddError("Failed to decode projects", err.Error())
return
}
elements := make([]attr.Value, 0, len(payload))
for _, project := range payload {
defaultEnv := types.StringNull()
for _, env := range project.Environments {
if env.IsDefault {
defaultEnv = types.StringValue(env.EnvironmentID)
break
}
}
object, diags := types.ObjectValue(projectSummaryType.AttrTypes, map[string]attr.Value{
"id": types.StringValue(project.ProjectID),
"name": types.StringValue(project.Name),
"description": optionalStringValue(project.Description),
"default_environment_id": defaultEnv,
})
resp.Diagnostics.Append(diags...)
elements = append(elements, object)
}
if resp.Diagnostics.HasError() {
return
}
list, diags := types.ListValue(projectSummaryType, elements)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &projectsDataSourceModel{Projects: list})...)
}
// -------------------------------------------------------- dokploy_environment
type environmentDataSourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Description types.String `tfsdk:"description"`
ProjectID types.String `tfsdk:"project_id"`
Env types.String `tfsdk:"env"`
IsDefault types.Bool `tfsdk:"is_default"`
CreatedAt types.String `tfsdk:"created_at"`
}
type environmentDataSource struct{ baseDataSource }
func newEnvironmentDataSource() datasource.DataSource { return &environmentDataSource{} }
func (d *environmentDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_environment"
}
func (d *environmentDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Look up an existing environment by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{Required: true, MarkdownDescription: "Environment identifier."},
"name": schema.StringAttribute{Computed: true, MarkdownDescription: "Environment name."},
"description": schema.StringAttribute{Computed: true, MarkdownDescription: "Environment description."},
"project_id": schema.StringAttribute{Computed: true, MarkdownDescription: "Owning project."},
"env": schema.StringAttribute{Computed: true, MarkdownDescription: "Environment-wide variables."},
"is_default": schema.BoolAttribute{Computed: true, MarkdownDescription: "Whether this is the project's default environment."},
"created_at": schema.StringAttribute{Computed: true, MarkdownDescription: "Creation timestamp."},
},
}
}
func (d *environmentDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config environmentDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
raw, err := d.api.Query(ctx, "environment.one", map[string]any{"environmentId": config.ID.ValueString()})
if err != nil {
resp.Diagnostics.AddError("Failed to read environment", err.Error())
return
}
var payload struct {
Name string `json:"name"`
Description *string `json:"description"`
ProjectID string `json:"projectId"`
Env string `json:"env"`
IsDefault bool `json:"isDefault"`
CreatedAt string `json:"createdAt"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
resp.Diagnostics.AddError("Failed to decode environment", err.Error())
return
}
config.Name = types.StringValue(payload.Name)
config.Description = optionalStringValue(payload.Description)
config.ProjectID = types.StringValue(payload.ProjectID)
config.Env = types.StringValue(payload.Env)
config.IsDefault = types.BoolValue(payload.IsDefault)
config.CreatedAt = types.StringValue(payload.CreatedAt)
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...)
}
// -------------------------------------------------------- dokploy_application
type applicationDataSourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
AppName types.String `tfsdk:"app_name"`
Description types.String `tfsdk:"description"`
EnvironmentID types.String `tfsdk:"environment_id"`
ApplicationStatus types.String `tfsdk:"application_status"`
SourceType types.String `tfsdk:"source_type"`
BuildType types.String `tfsdk:"build_type"`
CreatedAt types.String `tfsdk:"created_at"`
}
type applicationDataSource struct{ baseDataSource }
func newApplicationDataSource() datasource.DataSource { return &applicationDataSource{} }
func (d *applicationDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_application"
}
func (d *applicationDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Look up an existing application by ID.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{Required: true, MarkdownDescription: "Application identifier."},
"name": schema.StringAttribute{Computed: true, MarkdownDescription: "Application name."},
"app_name": schema.StringAttribute{Computed: true, MarkdownDescription: "Docker service name."},
"description": schema.StringAttribute{Computed: true, MarkdownDescription: "Application description."},
"environment_id": schema.StringAttribute{Computed: true, MarkdownDescription: "Owning environment."},
"application_status": schema.StringAttribute{Computed: true, MarkdownDescription: "Current status."},
"source_type": schema.StringAttribute{Computed: true, MarkdownDescription: "Source of the code or image."},
"build_type": schema.StringAttribute{Computed: true, MarkdownDescription: "Build strategy."},
"created_at": schema.StringAttribute{Computed: true, MarkdownDescription: "Creation timestamp."},
},
}
}
func (d *applicationDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config applicationDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
raw, err := d.api.Query(ctx, "application.one", map[string]any{"applicationId": config.ID.ValueString()})
if err != nil {
resp.Diagnostics.AddError("Failed to read application", err.Error())
return
}
var payload struct {
Name string `json:"name"`
AppName string `json:"appName"`
Description *string `json:"description"`
EnvironmentID string `json:"environmentId"`
ApplicationStatus string `json:"applicationStatus"`
SourceType string `json:"sourceType"`
BuildType string `json:"buildType"`
CreatedAt string `json:"createdAt"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
resp.Diagnostics.AddError("Failed to decode application", err.Error())
return
}
config.Name = types.StringValue(payload.Name)
config.AppName = types.StringValue(payload.AppName)
config.Description = optionalStringValue(payload.Description)
config.EnvironmentID = types.StringValue(payload.EnvironmentID)
config.ApplicationStatus = types.StringValue(payload.ApplicationStatus)
config.SourceType = types.StringValue(payload.SourceType)
config.BuildType = types.StringValue(payload.BuildType)
config.CreatedAt = types.StringValue(payload.CreatedAt)
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...)
}
// ------------------------------------------------------------ dokploy_servers
type serversDataSourceModel struct {
Servers types.List `tfsdk:"servers"`
}
type serversDataSource struct{ baseDataSource }
func newServersDataSource() datasource.DataSource { return &serversDataSource{} }
func (d *serversDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_servers"
}
var serverSummaryType = types.ObjectType{AttrTypes: map[string]attr.Type{
"id": types.StringType,
"name": types.StringType,
"ip_address": types.StringType,
"server_status": types.StringType,
}}
func (d *serversDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "List the remote servers registered with Dokploy. Use a server's `id` as the " +
"`server_id` of an application, stack, or database to deploy it away from the Dokploy host.",
Attributes: map[string]schema.Attribute{
"servers": schema.ListAttribute{
Computed: true,
ElementType: serverSummaryType,
MarkdownDescription: "All registered servers.",
},
},
}
}
func (d *serversDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) {
raw, err := d.api.Query(ctx, "server.all", nil)
if err != nil {
resp.Diagnostics.AddError("Failed to list servers", err.Error())
return
}
var payload []struct {
ServerID string `json:"serverId"`
Name string `json:"name"`
IPAddress *string `json:"ipAddress"`
ServerStatus *string `json:"serverStatus"`
}
if err := json.Unmarshal(raw, &payload); err != nil {
resp.Diagnostics.AddError("Failed to decode servers", err.Error())
return
}
elements := make([]attr.Value, 0, len(payload))
for _, server := range payload {
object, diags := types.ObjectValue(serverSummaryType.AttrTypes, map[string]attr.Value{
"id": types.StringValue(server.ServerID),
"name": types.StringValue(server.Name),
"ip_address": optionalStringValue(server.IPAddress),
"server_status": optionalStringValue(server.ServerStatus),
})
resp.Diagnostics.Append(diags...)
elements = append(elements, object)
}
if resp.Diagnostics.HasError() {
return
}
list, diags := types.ListValue(serverSummaryType, elements)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &serversDataSourceModel{Servers: list})...)
}
func optionalStringValue(value *string) types.String {
if value == nil {
return types.StringNull()
}
return types.StringValue(*value)
}