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) }