Skip to content

Commit

Permalink
feat(webhooks): consolidated resource and datasource logic (#332)
Browse files Browse the repository at this point in the history
* Add base api payloads and client for webhooks

* Temporary directive

* Add first iteration of webhook resource and datasource implementation

* use shared http helper

* fix schema attrs

* final

* datasource example

* test

* Generate Terraform Docs

---------

Co-authored-by: armalite <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 16, 2024
1 parent c05870b commit da29a47
Show file tree
Hide file tree
Showing 12 changed files with 1,087 additions and 0 deletions.
48 changes: 48 additions & 0 deletions docs/data-sources/webhook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "prefect_webhook Data Source - prefect"
subcategory: ""
description: |-
Get information about an existing Webhook, by name or ID.
Use this data source to obtain webhook-level attributes, such as ID, Name, Template, and more.
---

# prefect_webhook (Data Source)

Get information about an existing Webhook, by name or ID.
<br>
Use this data source to obtain webhook-level attributes, such as ID, Name, Template, and more.

## Example Usage

```terraform
# Query by ID
data "prefect_webhook" "example_by_id" {
id = "00000000-0000-0000-0000-000000000000"
}
# Query by name
data "prefect_webhook" "example_by_name" {
name = "my-webhook"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Optional

- `account_id` (String) Account ID (UUID), defaults to the account set in the provider
- `id` (String) Webhook ID (UUID)
- `name` (String) Name of the webhook
- `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)
- `description` (String) Description of the webhook
- `enabled` (Boolean) Whether the webhook is enabled
- `slug` (String) Slug of the webhook
- `template` (String) Template used by the webhook
- `updated` (String) Timestamp of when the resource was updated (RFC3339)
76 changes: 76 additions & 0 deletions docs/resources/webhook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "prefect_webhook Resource - prefect"
subcategory: ""
description: |-
The resource webhook represents a Prefect Cloud Webhook. Webhooks allow external services to trigger events in Prefect.
---

# prefect_webhook (Resource)

The resource `webhook` represents a Prefect Cloud Webhook. Webhooks allow external services to trigger events in Prefect.

## Example Usage

```terraform
resource "prefect_webhook" "example" {
name = "my-webhook"
description = "This is a webhook"
enabled = true
template = jsonencode({
event = "model.refreshed"
resource = {
"prefect.resource.id" = "product.models.{{ body.model }}"
"prefect.resource.name" = "{{ body.friendly_name }}"
"producing-team" = "Data Science"
}
})
}
# Use a JSON file to load a more complex template
resource "prefect_webhook" "example_with_file" {
name = "my-webhook"
description = "This is a webhook"
enabled = true
template = file("./webhook-template.json")
}
# Access the endpoint of the webhook.
output "endpoint" {
value = prefect_webhook.example_with_file.endpoint
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) Name of the webhook
- `template` (String) Template used by the webhook

### Optional

- `account_id` (String) Account ID (UUID), defaults to the account set in the provider
- `description` (String) Description of the webhook
- `enabled` (Boolean) Whether the webhook is enabled
- `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)
- `endpoint` (String) The fully-formed webhook endpoint, eg. https://api.prefect.cloud/SLUG
- `id` (String) Webhook ID (UUID)
- `updated` (String) Timestamp of when the resource was updated (RFC3339)

## Import

Import is supported using the following syntax:

```shell
# Prefect Webhooks can be imported using the format `workspace_id,id`
terraform import prefect_webhook.example 11111111-1111-1111-1111-111111111111,00000000-0000-0000-0000-000000000000

# You can also import by id only if you have a workspace_id set in your provider
terraform import prefect_webhook.example 00000000-0000-0000-0000-000000000000
```
9 changes: 9 additions & 0 deletions examples/data-sources/prefect_webhook/data-source.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Query by ID
data "prefect_webhook" "example_by_id" {
id = "00000000-0000-0000-0000-000000000000"
}

# Query by name
data "prefect_webhook" "example_by_name" {
name = "my-webhook"
}
5 changes: 5 additions & 0 deletions examples/resources/prefect_webhook/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Prefect Webhooks can be imported using the format `workspace_id,id`
terraform import prefect_webhook.example 11111111-1111-1111-1111-111111111111,00000000-0000-0000-0000-000000000000

# You can also import by id only if you have a workspace_id set in your provider
terraform import prefect_webhook.example 00000000-0000-0000-0000-000000000000
26 changes: 26 additions & 0 deletions examples/resources/prefect_webhook/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
resource "prefect_webhook" "example" {
name = "my-webhook"
description = "This is a webhook"
enabled = true
template = jsonencode({
event = "model.refreshed"
resource = {
"prefect.resource.id" = "product.models.{{ body.model }}"
"prefect.resource.name" = "{{ body.friendly_name }}"
"producing-team" = "Data Science"
}
})
}

# Use a JSON file to load a more complex template
resource "prefect_webhook" "example_with_file" {
name = "my-webhook"
description = "This is a webhook"
enabled = true
template = file("./webhook-template.json")
}

# Access the endpoint of the webhook.
output "endpoint" {
value = prefect_webhook.example_with_file.endpoint
}
1 change: 1 addition & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ type PrefectClient interface {
WorkPools(accountID uuid.UUID, workspaceID uuid.UUID) (WorkPoolsClient, error)
Variables(accountID uuid.UUID, workspaceID uuid.UUID) (VariablesClient, error)
ServiceAccounts(accountID uuid.UUID) (ServiceAccountsClient, error)
Webhooks(accountID, workspaceID uuid.UUID) (WebhooksClient, error)
}
66 changes: 66 additions & 0 deletions internal/api/webhooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package api

import (
"context"
"time"

"github.com/google/uuid"
)

type WebhooksClient interface {
Create(ctx context.Context, request WebhookCreateRequest) (*Webhook, error)
Get(ctx context.Context, webhookID string) (*Webhook, error)
List(ctx context.Context, names []string) ([]*Webhook, error)
Update(ctx context.Context, webhookID string, request WebhookUpdateRequest) error
Delete(ctx context.Context, webhookID string) error
}

/*** REQUEST DATA STRUCTS ***/

type WebhookCreateRequest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Enabled bool `json:"enabled"`
Template string `json:"template"`
}

type WebhookUpdateRequest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Enabled bool `json:"enabled"`
Template string `json:"template"`
}

/*** RESPONSE DATA STRUCTS ***/

type Webhook struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Enabled bool `json:"enabled"`
Template string `json:"template"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
AccountID uuid.UUID `json:"account"`
WorkspaceID uuid.UUID `json:"workspace"`
Slug string `json:"slug"`
}

type ErrorResponse struct {
Detail []ErrorDetail `json:"detail"`
}

type ErrorDetail struct {
Loc []string `json:"loc"`
Msg string `json:"msg"`
Type string `json:"type"`
}

// WebhookFilter defines filters when searching for webhooks.
type WebhookFilter struct {
Webhooks struct {
Name struct {
Any []string `json:"any_"`
} `json:"name,omitempty"`
} `json:"webhooks"`
}
137 changes: 137 additions & 0 deletions internal/client/webhooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package client

import (
"context"
"fmt"
"net/http"

"github.com/google/uuid"
"github.com/prefecthq/terraform-provider-prefect/internal/api"
)

var _ = api.WebhooksClient(&WebhooksClient{})

// WebhooksClient is a client for working with webhooks.
type WebhooksClient struct {
hc *http.Client
apiKey string
routePrefix string
}

// Webhooks returns a WebhooksClient.
//
//nolint:ireturn // required to support PrefectClient mocking
func (c *Client) Webhooks(accountID, workspaceID uuid.UUID) (api.WebhooksClient, error) {
if accountID == uuid.Nil {
accountID = c.defaultAccountID
}

if workspaceID == uuid.Nil {
workspaceID = c.defaultWorkspaceID
}

if err := validateCloudEndpoint(c.endpoint, accountID, workspaceID); err != nil {
return nil, err
}

return &WebhooksClient{
hc: c.hc,
apiKey: c.apiKey,
routePrefix: getWorkspaceScopedURL(c.endpoint, accountID, workspaceID, "webhooks"),
}, nil
}

// Create creates a new webhook.
func (c *WebhooksClient) Create(ctx context.Context, createPayload api.WebhookCreateRequest) (*api.Webhook, error) {
cfg := requestConfig{
method: http.MethodPost,
url: c.routePrefix + "/",
body: &createPayload,
successCodes: successCodesStatusCreated,
apiKey: c.apiKey,
}

var webhook api.Webhook
if err := requestWithDecodeResponse(ctx, c.hc, cfg, &webhook); err != nil {
return nil, fmt.Errorf("failed to create webhook: %w", err)
}

return &webhook, nil
}

// Get returns details for a webhook by ID.
func (c *WebhooksClient) Get(ctx context.Context, webhookID string) (*api.Webhook, error) {
cfg := requestConfig{
method: http.MethodGet,
url: c.routePrefix + "/" + webhookID,
successCodes: successCodesStatusOK,
body: http.NoBody,
apiKey: c.apiKey,
}

var webhook api.Webhook
if err := requestWithDecodeResponse(ctx, c.hc, cfg, &webhook); err != nil {
return nil, fmt.Errorf("failed to get webhook: %w", err)
}

return &webhook, nil
}

// Update modifies an existing webhook by ID.
func (c *WebhooksClient) Update(ctx context.Context, webhookID string, updatePayload api.WebhookUpdateRequest) error {
cfg := requestConfig{
method: http.MethodPut,
url: c.routePrefix + "/" + webhookID,
body: &updatePayload,
successCodes: successCodesStatusOKOrNoContent,
apiKey: c.apiKey,
}

resp, err := request(ctx, c.hc, cfg)
if err != nil {
return fmt.Errorf("failed to update webhook: %w", err)
}
defer resp.Body.Close()

return nil
}

// Delete removes a webhook by ID.
func (c *WebhooksClient) Delete(ctx context.Context, webhookID string) error {
cfg := requestConfig{
method: http.MethodDelete,
url: c.routePrefix + "/" + webhookID,
successCodes: successCodesStatusOKOrNoContent,
body: http.NoBody,
apiKey: c.apiKey,
}

resp, err := request(ctx, c.hc, cfg)
if err != nil {
return fmt.Errorf("failed to delete webhook: %w", err)
}
defer resp.Body.Close()

return nil
}

// List returns a list of webhooks matching filter criteria.
func (c *WebhooksClient) List(ctx context.Context, names []string) ([]*api.Webhook, error) {
filter := api.WebhookFilter{}
filter.Webhooks.Name.Any = names

cfg := requestConfig{
method: http.MethodGet,
url: c.routePrefix + "/",
body: http.NoBody,
successCodes: successCodesStatusOK,
apiKey: c.apiKey,
}

var webhooks []*api.Webhook
if err := requestWithDecodeResponse(ctx, c.hc, cfg, &webhooks); err != nil {
return nil, fmt.Errorf("failed to list webhooks: %w", err)
}

return webhooks, nil
}
Loading

0 comments on commit da29a47

Please sign in to comment.