Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(webhooks): add optional service_account_id for authZ #348

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion create-dev-testfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ main() {

resource="prefect_${resource}"

dev_file_target="./dev/${name}"
dev_file_target="${PWD}/dev/${name}"

mkdir -p $dev_file_target

Expand Down
1 change: 1 addition & 0 deletions docs/data-sources/webhook.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ data "prefect_webhook" "example_by_name" {
- `created` (String) Timestamp of when the resource was created (RFC3339)
- `description` (String) Description of the webhook
- `enabled` (Boolean) Whether the webhook is enabled
- `service_account_id` (String) ID of the Service Account to which this webhook belongs. `Pro` and `Enterprise` customers can assign a Service Account to a webhook to enhance security.
- `slug` (String) Slug of the webhook
- `template` (String) Template used by the webhook
- `updated` (String) Timestamp of when the resource was updated (RFC3339)
25 changes: 22 additions & 3 deletions docs/resources/webhook.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,35 @@ resource "prefect_webhook" "example" {
})
}

# Use a JSON file to load a more complex template
# Optionally, 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")
}

# Pro / Enterprise customers can assign a Service Account to a webhook to enhance security.
# If set, the webhook request will be authorized with the Service Account's API key.
# NOTE: if the Service Account is deleted, the associated Webhook is also deleted.
resource "prefect_service_account" "authorized" {
name = "my-service-account"
}
resource "prefect_webhook" "example_with_service_account" {
name = "my-webhook-with-auth"
description = "This is an authorized webhook"
enabled = true
template = file("./webhook-template.json")
service_account_id = prefect_service_account.authorized.id
}

# Access the endpoint of the webhook.
output "endpoint" {
value = prefect_webhook.example_with_file.endpoint
output "endpoints" {
value = {
example = prefect_webhook.example.endpoint
example_with_file = prefect_webhook.example_with_file.endpoint
example_with_service_account = prefect_webhook.example_with_service_account.endpoint
}
}
```

Expand All @@ -54,6 +72,7 @@ output "endpoint" {
- `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
- `service_account_id` (String) ID of the Service Account to which this webhook belongs. `Pro` and `Enterprise` customers can assign a Service Account to a webhook to enhance security. If set, the webhook request will be authorized with the Service Account's API key.
- `workspace_id` (String) Workspace ID (UUID), defaults to the workspace set in the provider

### Read-Only
Expand Down
24 changes: 21 additions & 3 deletions examples/resources/prefect_webhook/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,33 @@ resource "prefect_webhook" "example" {
})
}

# Use a JSON file to load a more complex template
# Optionally, 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")
}

# Pro / Enterprise customers can assign a Service Account to a webhook to enhance security.
# If set, the webhook request will be authorized with the Service Account's API key.
# NOTE: if the Service Account is deleted, the associated Webhook is also deleted.
resource "prefect_service_account" "authorized" {
name = "my-service-account"
}
resource "prefect_webhook" "example_with_service_account" {
name = "my-webhook-with-auth"
description = "This is an authorized webhook"
enabled = true
template = file("./webhook-template.json")
service_account_id = prefect_service_account.authorized.id
}

# Access the endpoint of the webhook.
output "endpoint" {
value = prefect_webhook.example_with_file.endpoint
output "endpoints" {
value = {
example = prefect_webhook.example.endpoint
example_with_file = prefect_webhook.example_with_file.endpoint
example_with_service_account = prefect_webhook.example_with_service_account.endpoint
}
}
32 changes: 13 additions & 19 deletions internal/api/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package api

import (
"context"
"time"

"github.com/google/uuid"
)
Expand All @@ -15,32 +14,27 @@ type WebhooksClient interface {
Delete(ctx context.Context, webhookID string) error
}

/*** REQUEST DATA STRUCTS ***/
type WebhookCore struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Enabled bool `json:"enabled"`
Template string `json:"template"`
ServiceAccountID *uuid.UUID `json:"service_account_id"`
}

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

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

/*** RESPONSE DATA STRUCTS ***/

// Response Schemas.
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"`
BaseModel
WebhookCore
AccountID uuid.UUID `json:"account"`
WorkspaceID uuid.UUID `json:"workspace"`
Slug string `json:"slug"`
Expand Down
31 changes: 19 additions & 12 deletions internal/provider/datasources/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@ type WebhookDataSource struct {

// WebhookDataSourceModel defines the Terraform data source model.
type WebhookDataSourceModel struct {
ID customtypes.UUIDValue `tfsdk:"id"`
Created customtypes.TimestampValue `tfsdk:"created"`
Updated customtypes.TimestampValue `tfsdk:"updated"`
Name types.String `tfsdk:"name"`
Description types.String `tfsdk:"description"`
Enabled types.Bool `tfsdk:"enabled"`
Template types.String `tfsdk:"template"`
AccountID customtypes.UUIDValue `tfsdk:"account_id"`
WorkspaceID customtypes.UUIDValue `tfsdk:"workspace_id"`
Slug types.String `tfsdk:"slug"`
ID customtypes.UUIDValue `tfsdk:"id"`
Created customtypes.TimestampValue `tfsdk:"created"`
Updated customtypes.TimestampValue `tfsdk:"updated"`
Name types.String `tfsdk:"name"`
Description types.String `tfsdk:"description"`
Enabled types.Bool `tfsdk:"enabled"`
Template types.String `tfsdk:"template"`
AccountID customtypes.UUIDValue `tfsdk:"account_id"`
WorkspaceID customtypes.UUIDValue `tfsdk:"workspace_id"`
Slug types.String `tfsdk:"slug"`
ServiceAccountID customtypes.UUIDValue `tfsdk:"service_account_id"`
}

// NewWebhookDataSource returns a new WebhookDataSource.
Expand Down Expand Up @@ -110,6 +111,11 @@ var webhookAttributes = map[string]schema.Attribute{
Computed: true,
Description: "Slug of the webhook",
},
"service_account_id": schema.StringAttribute{
CustomType: customtypes.UUIDType{},
Description: "ID of the Service Account to which this webhook belongs. `Pro` and `Enterprise` customers can assign a Service Account to a webhook to enhance security.",
Computed: true,
},
}

// Schema defines the schema for the data source.
Expand Down Expand Up @@ -189,8 +195,8 @@ func (d *WebhookDataSource) Read(ctx context.Context, req datasource.ReadRequest
}

model.ID = customtypes.NewUUIDValue(webhook.ID)
model.Created = customtypes.NewTimestampPointerValue(&webhook.Created)
model.Updated = customtypes.NewTimestampPointerValue(&webhook.Updated)
model.Created = customtypes.NewTimestampPointerValue(webhook.Created)
model.Updated = customtypes.NewTimestampPointerValue(webhook.Updated)

model.Name = types.StringValue(webhook.Name)
model.Description = types.StringValue(webhook.Description)
Expand All @@ -199,6 +205,7 @@ func (d *WebhookDataSource) Read(ctx context.Context, req datasource.ReadRequest
model.AccountID = customtypes.NewUUIDValue(webhook.AccountID)
model.WorkspaceID = customtypes.NewUUIDValue(webhook.WorkspaceID)
model.Slug = types.StringValue(webhook.Slug)
model.ServiceAccountID = customtypes.NewUUIDPointerValue(webhook.ServiceAccountID)

resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
if resp.Diagnostics.HasError() {
Expand Down
67 changes: 43 additions & 24 deletions internal/provider/resources/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,17 @@ type WebhookResource struct {
}

type WebhookResourceModel struct {
ID types.String `tfsdk:"id"`
Created customtypes.TimestampValue `tfsdk:"created"`
Updated customtypes.TimestampValue `tfsdk:"updated"`
Name types.String `tfsdk:"name"`
Description types.String `tfsdk:"description"`
Enabled types.Bool `tfsdk:"enabled"`
Template types.String `tfsdk:"template"`
AccountID customtypes.UUIDValue `tfsdk:"account_id"`
WorkspaceID customtypes.UUIDValue `tfsdk:"workspace_id"`
Endpoint types.String `tfsdk:"endpoint"`
ID types.String `tfsdk:"id"`
Created customtypes.TimestampValue `tfsdk:"created"`
Updated customtypes.TimestampValue `tfsdk:"updated"`
Name types.String `tfsdk:"name"`
Description types.String `tfsdk:"description"`
Enabled types.Bool `tfsdk:"enabled"`
Template types.String `tfsdk:"template"`
AccountID customtypes.UUIDValue `tfsdk:"account_id"`
WorkspaceID customtypes.UUIDValue `tfsdk:"workspace_id"`
Endpoint types.String `tfsdk:"endpoint"`
ServiceAccountID customtypes.UUIDValue `tfsdk:"service_account_id"`
}

// NewWebhookResource returns a new WebhookResource.
Expand Down Expand Up @@ -114,36 +115,48 @@ func (r *WebhookResource) Schema(_ context.Context, _ resource.SchemaRequest, re
Description: "Timestamp of when the resource was updated (RFC3339)",
},
"account_id": schema.StringAttribute{
Computed: true,
Optional: true,
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
CustomType: customtypes.UUIDType{},
Description: "Account ID (UUID), defaults to the account set in the provider",
},
"workspace_id": schema.StringAttribute{
Computed: true,
Optional: true,
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
CustomType: customtypes.UUIDType{},
Description: "Workspace ID (UUID), defaults to the workspace set in the provider",
},
"endpoint": schema.StringAttribute{
Computed: true,
Description: "The fully-formed webhook endpoint, eg. https://api.prefect.cloud/SLUG",
},
"service_account_id": schema.StringAttribute{
Optional: true,
CustomType: customtypes.UUIDType{},
Description: "ID of the Service Account to which this webhook belongs. `Pro` and `Enterprise` customers can assign a Service Account to a webhook to enhance security. If set, the webhook request will be authorized with the Service Account's API key.",
},
},
}
}

// copyWebhookResponseToModel maps an API response to a model that is saved in Terraform state.
func copyWebhookResponseToModel(webhook *api.Webhook, tfModel *WebhookResourceModel) {
tfModel.ID = types.StringValue(webhook.ID.String())
tfModel.Created = customtypes.NewTimestampPointerValue(&webhook.Created)
tfModel.Updated = customtypes.NewTimestampPointerValue(&webhook.Updated)
tfModel.Created = customtypes.NewTimestampPointerValue(webhook.Created)
tfModel.Updated = customtypes.NewTimestampPointerValue(webhook.Updated)
tfModel.Name = types.StringValue(webhook.Name)
tfModel.Description = types.StringValue(webhook.Description)
tfModel.Enabled = types.BoolValue(webhook.Enabled)
tfModel.Template = types.StringValue(webhook.Template)
tfModel.AccountID = customtypes.NewUUIDValue(webhook.AccountID)
tfModel.WorkspaceID = customtypes.NewUUIDValue(webhook.WorkspaceID)
tfModel.ServiceAccountID = customtypes.NewUUIDPointerValue(webhook.ServiceAccountID)
}

// Create creates the resource and sets the initial Terraform state.
Expand All @@ -163,10 +176,13 @@ func (r *WebhookResource) Create(ctx context.Context, req resource.CreateRequest
}

createReq := api.WebhookCreateRequest{
Name: plan.Name.ValueString(),
Description: plan.Description.ValueString(),
Enabled: plan.Enabled.ValueBool(),
Template: plan.Template.ValueString(),
WebhookCore: api.WebhookCore{
Name: plan.Name.ValueString(),
Description: plan.Description.ValueString(),
Enabled: plan.Enabled.ValueBool(),
Template: plan.Template.ValueString(),
ServiceAccountID: plan.ServiceAccountID.ValueUUIDPointer(),
},
}

webhook, err := webhookClient.Create(ctx, createReq)
Expand Down Expand Up @@ -260,10 +276,13 @@ func (r *WebhookResource) Update(ctx context.Context, req resource.UpdateRequest
}

updateReq := api.WebhookUpdateRequest{
Name: plan.Name.ValueString(),
Description: plan.Description.ValueString(),
Enabled: plan.Enabled.ValueBool(),
Template: plan.Template.ValueString(),
WebhookCore: api.WebhookCore{
Name: plan.Name.ValueString(),
Description: plan.Description.ValueString(),
Enabled: plan.Enabled.ValueBool(),
Template: plan.Template.ValueString(),
ServiceAccountID: plan.ServiceAccountID.ValueUUIDPointer(),
},
}

err = client.Update(ctx, state.ID.ValueString(), updateReq)
Expand Down
29 changes: 29 additions & 0 deletions internal/provider/resources/webhooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ resource "prefect_webhook" "%s" {
`, workspace, name, name, template, enabled)
}

func fixtureAccWebhookWithServiceAccount(workspace, name, template string, enabled bool) string {
return fmt.Sprintf(`
%s

resource "prefect_service_account" "service_account" {
name = "service-account"
account_role_name = "Member"
}

resource "prefect_webhook" "%s" {
name = "%s"
template = jsonencode(%s)
enabled = %t
workspace_id = prefect_workspace.test.id
service_account_id = prefect_service_account.service_account.id
}
`, workspace, name, name, template, enabled)
}

const webhookTemplateDynamic = `
{
"event": "model.refreshed",
Expand Down Expand Up @@ -89,6 +108,16 @@ func TestAccResource_webhook(t *testing.T) {
resource.TestCheckResourceAttr(webhookResourceName, "enabled", "true"),
),
},
{
// Check that a service account can be set
Config: fixtureAccWebhookWithServiceAccount(workspace.Resource, randomName, webhookTemplateStatic, true),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckWebhookExists(webhookResourceName, &webhook),
resource.TestCheckResourceAttr(webhookResourceName, "name", randomName),
resource.TestCheckResourceAttr(webhookResourceName, "enabled", "true"),
resource.TestCheckResourceAttrSet(webhookResourceName, "service_account_id"),
),
},
// Import State checks - import by name (dynamic)
{
ImportState: true,
Expand Down