diff --git a/README.md b/README.md
index bc727212..f00fd0e5 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ Check back with us to see new additions and improvements - and please don't hesi
| Account Member | ✓ | | | ✓ |
| Account Role | ✓ | | | ✓ |
| Account | ✓ | ✓ | ✓ | ✓ |
-| Block | | ✓ | ✓ | |
+| Block | ✓ | ✓ | ✓ | |
| Service Account | ✓ | ✓ | ✓ | ✓ |
| Team | ✓ | | | ✓ |
| Variable | ✓ | ✓ | ✓ | |
diff --git a/docs/data-sources/block.md b/docs/data-sources/block.md
new file mode 100644
index 00000000..6d2f3a63
--- /dev/null
+++ b/docs/data-sources/block.md
@@ -0,0 +1,59 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "prefect_block Data Source - prefect"
+subcategory: ""
+description: |-
+ Get information about an existing Block by either:
+ ID, orblock type name and block name
+
+ If the ID is provided, then the block type name and block name will be ignored.
+
+ Use this data source to obtain Block-specific attributes, such as the data.
+---
+
+# prefect_block (Data Source)
+
+Get information about an existing Block by either:
+- ID, or
+- block type name and block name
+
+If the ID is provided, then the block type name and block name will be ignored.
+
+Use this data source to obtain Block-specific attributes, such as the data.
+
+## Example Usage
+
+```terraform
+# Get block by ID using Terraform ID reference.
+data "prefect_block" "existing_by_id" {
+ id = prefect_block.my_existing_block.id
+}
+
+# Get block by ID string.
+data "prefect_block" "existing_by_id_string" {
+ id = "00000000-0000-0000-0000-000000000000"
+}
+
+# Get block by type slug and name.
+data "prefect_block" "existing_by_id_string" {
+ name = "my_existing_block"
+ type_slug = "secret"
+}
+```
+
+
+## Schema
+
+### Optional
+
+- `account_id` (String) Account ID (UUID), defaults to the account set in the provider
+- `id` (String) Block ID (UUID)
+- `name` (String) Name of the block
+- `type_slug` (String) Block type slug
+- `workspace_id` (String) Workspace ID (UUID), defaults to the workspace set in the provider
+
+### Read-Only
+
+- `created` (String) Timestamp of when the resource was created (RFC3339)
+- `data` (String, Sensitive) The user-inputted Block payload, as a JSON string. The value's schema will depend on the selected `type` slug. Use `prefect block types inspect ` to view the data schema for a given Block type.
+- `updated` (String) Timestamp of when the resource was updated (RFC3339)
diff --git a/examples/data-sources/prefect_block/data-source.tf b/examples/data-sources/prefect_block/data-source.tf
new file mode 100644
index 00000000..fa72a9bc
--- /dev/null
+++ b/examples/data-sources/prefect_block/data-source.tf
@@ -0,0 +1,15 @@
+# Get block by ID using Terraform ID reference.
+data "prefect_block" "existing_by_id" {
+ id = prefect_block.my_existing_block.id
+}
+
+# Get block by ID string.
+data "prefect_block" "existing_by_id_string" {
+ id = "00000000-0000-0000-0000-000000000000"
+}
+
+# Get block by type slug and name.
+data "prefect_block" "existing_by_id_string" {
+ name = "my_existing_block"
+ type_slug = "secret"
+}
diff --git a/internal/api/block_documents.go b/internal/api/block_documents.go
index 6e7a35fc..3b218107 100644
--- a/internal/api/block_documents.go
+++ b/internal/api/block_documents.go
@@ -8,6 +8,7 @@ import (
type BlockDocumentClient interface {
Get(ctx context.Context, id uuid.UUID) (*BlockDocument, error)
+ GetByName(ctx context.Context, typeSlug, name string) (*BlockDocument, error)
Create(ctx context.Context, payload BlockDocumentCreate) (*BlockDocument, error)
Update(ctx context.Context, id uuid.UUID, payload BlockDocumentUpdate) error
Delete(ctx context.Context, id uuid.UUID) error
diff --git a/internal/client/block_documents.go b/internal/client/block_documents.go
index d27a7239..d52f7ba5 100644
--- a/internal/client/block_documents.go
+++ b/internal/client/block_documents.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
+ "strings"
"github.com/google/uuid"
"github.com/prefecthq/terraform-provider-prefect/internal/api"
@@ -75,6 +76,39 @@ func (c *BlockDocumentClient) Get(ctx context.Context, id uuid.UUID) (*api.Block
return &blockDocument, nil
}
+func (c *BlockDocumentClient) GetByName(ctx context.Context, typeSlug, name string) (*api.BlockDocument, error) {
+ // This URL is a little different, as it starts with 'block_types' instead of 'block_documents'.
+ newRoutePrefix := fmt.Sprintf("block_types/slug/%s/block_documents/name/%s", typeSlug, name)
+ reqURL := strings.ReplaceAll(c.routePrefix, "block_documents", newRoutePrefix)
+ reqURL = fmt.Sprintf("%s?include_secrets=true", reqURL)
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, http.NoBody)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+
+ setDefaultHeaders(req, c.apiKey)
+
+ resp, err := c.hc.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("http error: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ errorBody, _ := io.ReadAll(resp.Body)
+
+ return nil, fmt.Errorf("status code %s, error=%s", resp.Status, errorBody)
+ }
+
+ var blockDocument api.BlockDocument
+ if err := json.NewDecoder(resp.Body).Decode(&blockDocument); err != nil {
+ return nil, fmt.Errorf("failed to decode response: %w", err)
+ }
+
+ return &blockDocument, nil
+}
+
func (c *BlockDocumentClient) Create(ctx context.Context, payload api.BlockDocumentCreate) (*api.BlockDocument, error) {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&payload); err != nil {
diff --git a/internal/provider/datasources/block.go b/internal/provider/datasources/block.go
new file mode 100644
index 00000000..fd16bb2d
--- /dev/null
+++ b/internal/provider/datasources/block.go
@@ -0,0 +1,198 @@
+package datasources
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/prefecthq/terraform-provider-prefect/internal/api"
+ "github.com/prefecthq/terraform-provider-prefect/internal/provider/customtypes"
+ "github.com/prefecthq/terraform-provider-prefect/internal/provider/helpers"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ datasource.DataSource = &blockDataSource{}
+ _ datasource.DataSourceWithConfigure = &blockDataSource{}
+)
+
+// blockDataSource is the data source implementation.
+type blockDataSource struct {
+ client api.PrefectClient
+}
+
+// BlockDataSourceModel defines the Terraform data source model.
+type BlockDataSourceModel struct {
+ ID customtypes.UUIDValue `tfsdk:"id"`
+ Created customtypes.TimestampValue `tfsdk:"created"`
+ Updated customtypes.TimestampValue `tfsdk:"updated"`
+ AccountID customtypes.UUIDValue `tfsdk:"account_id"`
+ WorkspaceID customtypes.UUIDValue `tfsdk:"workspace_id"`
+
+ Name types.String `tfsdk:"name"`
+ Data jsontypes.Normalized `tfsdk:"data"`
+ TypeSlug types.String `tfsdk:"type_slug"`
+}
+
+// NewBlockDataSource is a helper function to simplify the provider implementation.
+//
+//nolint:ireturn // required by Terraform API
+func NewBlockDataSource() datasource.DataSource {
+ return &blockDataSource{}
+}
+
+// Metadata returns the data source type name.
+func (d *blockDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_block"
+}
+
+// Schema defines the scema for the data source.
+func (d *blockDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Description: `
+Get information about an existing Block by either:
+- ID, or
+- block type name and block name
+
+If the ID is provided, then the block type name and block name will be ignored.
+
+Use this data source to obtain Block-specific attributes, such as the data.
+`,
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ CustomType: customtypes.UUIDType{},
+ Description: "Block ID (UUID)",
+ Optional: true,
+ },
+ "created": schema.StringAttribute{
+ Computed: true,
+ CustomType: customtypes.TimestampType{},
+ Description: "Timestamp of when the resource was created (RFC3339)",
+ },
+ "updated": schema.StringAttribute{
+ Computed: true,
+ CustomType: customtypes.TimestampType{},
+ Description: "Timestamp of when the resource was updated (RFC3339)",
+ },
+ "account_id": schema.StringAttribute{
+ CustomType: customtypes.UUIDType{},
+ Description: "Account ID (UUID), defaults to the account set in the provider",
+ Optional: true,
+ },
+ "workspace_id": schema.StringAttribute{
+ CustomType: customtypes.UUIDType{},
+ Description: "Workspace ID (UUID), defaults to the workspace set in the provider",
+ Optional: true,
+ },
+ "name": schema.StringAttribute{
+ Computed: true,
+ Description: "Name of the block",
+ Optional: true,
+ },
+ "data": schema.StringAttribute{
+ Computed: true,
+ Sensitive: true,
+ CustomType: jsontypes.NormalizedType{},
+ Description: "The user-inputted Block payload, as a JSON string. The value's schema will depend on the selected `type` slug. Use `prefect block types inspect ` to view the data schema for a given Block type.",
+ },
+ "type_slug": schema.StringAttribute{
+ Computed: true,
+ Description: "Block type slug",
+ Optional: true,
+ },
+ },
+ }
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *blockDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var state BlockDataSourceModel
+
+ diag := req.Config.Get(ctx, &state)
+ resp.Diagnostics.Append(diag...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ client, err := d.client.BlockDocuments(state.AccountID.ValueUUID(), state.WorkspaceID.ValueUUID())
+ if err != nil {
+ resp.Diagnostics.Append(helpers.CreateClientErrorDiagnostic("Block", err))
+
+ return
+ }
+
+ var block *api.BlockDocument
+
+ switch {
+ case !state.ID.IsNull():
+ block, err = client.Get(ctx, state.ID.ValueUUID())
+ case !state.Name.IsNull() && !state.TypeSlug.IsNull():
+ block, err = client.GetByName(ctx, state.TypeSlug.ValueString(), state.Name.ValueString())
+ default:
+ resp.Diagnostics.AddError(
+ "Insufficient search criteria provided",
+ "Provide either the ID, or the block type name and block name.",
+ )
+
+ return
+ }
+
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error refreshing block state",
+ fmt.Sprintf("Could not read block, unexpected error: %s", err.Error()),
+ )
+
+ return
+ }
+
+ state.ID = customtypes.NewUUIDValue(block.ID)
+ state.Created = customtypes.NewTimestampPointerValue(block.Created)
+ state.Updated = customtypes.NewTimestampPointerValue(block.Updated)
+
+ state.Name = types.StringValue(block.Name)
+ state.TypeSlug = types.StringValue(block.BlockType.Slug)
+
+ byteSlice, err := json.Marshal(block.Data)
+ if err != nil {
+ resp.Diagnostics.AddAttributeError(
+ path.Root("data"),
+ "Failed to serialize Block Data",
+ fmt.Sprintf("Could not serialize Block Data as JSON string: %s", err.Error()),
+ )
+
+ return
+ }
+
+ state.Data = jsontypes.NewNormalizedValue(string(byteSlice))
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+}
+
+// Configure initializes runtime state for the data source.
+func (d *blockDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ client, ok := req.ProviderData.(api.PrefectClient)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected api.PrefectClient, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+
+ return
+ }
+
+ d.client = client
+}
diff --git a/internal/provider/datasources/block_test.go b/internal/provider/datasources/block_test.go
new file mode 100644
index 00000000..7d8289da
--- /dev/null
+++ b/internal/provider/datasources/block_test.go
@@ -0,0 +1,85 @@
+package datasources_test
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/prefecthq/terraform-provider-prefect/internal/testutils"
+)
+
+func fixtureAccBlockByName(name string) string {
+ aID := os.Getenv("PREFECT_CLOUD_ACCOUNT_ID")
+
+ return fmt.Sprintf(`
+data "prefect_workspace" "evergreen" {
+ handle = "github-ci-tests"
+}
+
+resource "prefect_block" "%s" {
+ name = "%s"
+ type_slug = "secret"
+
+ data = jsonencode({
+ "someKey" : "someValue"
+ })
+
+ account_id = "%s"
+ workspace_id = data.prefect_workspace.evergreen.id
+}
+
+data "prefect_block" "my_existing_secret_by_id" {
+ id = prefect_block.%s.id
+
+ account_id = "%s"
+ workspace_id = data.prefect_workspace.evergreen.id
+
+ depends_on = [prefect_block.%s]
+}
+
+data "prefect_block" "my_existing_secret_by_name" {
+ name = "%s"
+ type_slug = "secret"
+
+ account_id = "%s"
+ workspace_id = data.prefect_workspace.evergreen.id
+
+ depends_on = [prefect_block.%s]
+}
+`, name, name, aID, name, aID, name, name, aID, name)
+}
+
+//nolint:paralleltest // we use the resource.ParallelTest helper instead
+func TestAccDatasource_block(t *testing.T) {
+ datasourceNameByID := "data.prefect_block.my_existing_secret_by_id"
+ datasourceNameByName := "data.prefect_block.my_existing_secret_by_name"
+
+ blockName := "my-block"
+ blockValue := "{\"someKey\":\"someValue\"}"
+
+ resource.ParallelTest(t, resource.TestCase{
+ ProtoV6ProviderFactories: testutils.TestAccProtoV6ProviderFactories,
+ PreCheck: func() { testutils.AccTestPreCheck(t) },
+ Steps: []resource.TestStep{
+ {
+ // Test block datasource by ID.
+ Config: fixtureAccBlockByName(blockName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet(datasourceNameByID, "id"),
+ resource.TestCheckResourceAttr(datasourceNameByID, "name", blockName),
+ resource.TestCheckResourceAttr(datasourceNameByID, "data", blockValue),
+ ),
+ },
+ {
+ // Test block datasource by name.
+ Config: fixtureAccBlockByName(blockName),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet(datasourceNameByName, "id"),
+ resource.TestCheckResourceAttr(datasourceNameByName, "name", blockName),
+ resource.TestCheckResourceAttr(datasourceNameByName, "data", blockValue),
+ ),
+ },
+ },
+ })
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 0aed2c16..8d311493 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -220,6 +220,7 @@ func (p *PrefectProvider) DataSources(_ context.Context) []func() datasource.Dat
datasources.NewAccountMemberDataSource,
datasources.NewAccountMembersDataSource,
datasources.NewAccountRoleDataSource,
+ datasources.NewBlockDataSource,
datasources.NewServiceAccountDataSource,
datasources.NewTeamDataSource,
datasources.NewTeamsDataSource,