Skip to content

Commit

Permalink
feat(blocks): add Block data source (#209)
Browse files Browse the repository at this point in the history
Adds block data source along with docs and tests.

Closes #175
  • Loading branch information
mitchnielsen authored Jun 11, 2024
1 parent ff8c9b6 commit 0258c8e
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | ✓ | ✓ | ✓ | |
Expand Down
59 changes: 59 additions & 0 deletions docs/data-sources/block.md
Original file line number Diff line number Diff line change
@@ -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
<br>
If the ID is provided, then the block type name and block name will be ignored.
<br>
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 generated by tfplugindocs -->
## 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 <slug>` to view the data schema for a given Block type.
- `updated` (String) Timestamp of when the resource was updated (RFC3339)
15 changes: 15 additions & 0 deletions examples/data-sources/prefect_block/data-source.tf
Original file line number Diff line number Diff line change
@@ -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"
}
1 change: 1 addition & 0 deletions internal/api/block_documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions internal/client/block_documents.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"strings"

"github.com/google/uuid"
"github.com/prefecthq/terraform-provider-prefect/internal/api"
Expand Down Expand Up @@ -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 {
Expand Down
198 changes: 198 additions & 0 deletions internal/provider/datasources/block.go
Original file line number Diff line number Diff line change
@@ -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
<br>
If the ID is provided, then the block type name and block name will be ignored.
<br>
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 <slug>` 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
}
Loading

0 comments on commit 0258c8e

Please sign in to comment.