From c75a0ae2f7e8cfc9e80eed50a40511280558a8a6 Mon Sep 17 00:00:00 2001 From: Edward Park Date: Tue, 24 Dec 2024 11:05:44 -0800 Subject: [PATCH 1/3] add service_account_id to webhook resource schema --- internal/provider/resources/webhooks.go | 26 +++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/internal/provider/resources/webhooks.go b/internal/provider/resources/webhooks.go index cbff532..6069092 100644 --- a/internal/provider/resources/webhooks.go +++ b/internal/provider/resources/webhooks.go @@ -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. @@ -129,6 +130,11 @@ func (r *WebhookResource) Schema(_ context.Context, _ resource.SchemaRequest, re Computed: true, Description: "The fully-formed webhook endpoint, eg. `https://api.prefect.cloud/hooks/`", }, + "service_account_id": schema.StringAttribute{ + Optional: true, + CustomType: customtypes.UUIDType{}, + Description: "ID of the Service Account to which this webhook belongs. If set, the webhook request will be authorized with the Service Account's API key.", + }, }, } } From 9f50ac5a6b501c18c2337c877416e39157d8bde5 Mon Sep 17 00:00:00 2001 From: Edward Park Date: Thu, 26 Dec 2024 10:36:59 -0800 Subject: [PATCH 2/3] tests and docs --- create-dev-testfile.sh | 2 +- .../resources/prefect_webhook/resource.tf | 24 +++++++++-- internal/api/webhooks.go | 32 ++++++-------- internal/provider/datasources/webhooks.go | 31 +++++++------ internal/provider/resources/webhooks.go | 43 ++++++++++++------- internal/provider/resources/webhooks_test.go | 29 +++++++++++++ 6 files changed, 111 insertions(+), 50 deletions(-) diff --git a/create-dev-testfile.sh b/create-dev-testfile.sh index 1e7399e..31848f3 100644 --- a/create-dev-testfile.sh +++ b/create-dev-testfile.sh @@ -18,7 +18,7 @@ main() { resource="prefect_${resource}" - dev_file_target="./dev/${name}" + dev_file_target="${PWD}/dev/${name}" mkdir -p $dev_file_target diff --git a/examples/resources/prefect_webhook/resource.tf b/examples/resources/prefect_webhook/resource.tf index 182632a..8bec41c 100644 --- a/examples/resources/prefect_webhook/resource.tf +++ b/examples/resources/prefect_webhook/resource.tf @@ -12,7 +12,7 @@ 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" @@ -20,7 +20,25 @@ resource "prefect_webhook" "example_with_file" { 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 + } } diff --git a/internal/api/webhooks.go b/internal/api/webhooks.go index 7a568b0..cc3707f 100644 --- a/internal/api/webhooks.go +++ b/internal/api/webhooks.go @@ -2,7 +2,6 @@ package api import ( "context" - "time" "github.com/google/uuid" ) @@ -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"` diff --git a/internal/provider/datasources/webhooks.go b/internal/provider/datasources/webhooks.go index b57aaac..4186e0e 100644 --- a/internal/provider/datasources/webhooks.go +++ b/internal/provider/datasources/webhooks.go @@ -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. @@ -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. @@ -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) @@ -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() { diff --git a/internal/provider/resources/webhooks.go b/internal/provider/resources/webhooks.go index 6069092..8d4b39b 100644 --- a/internal/provider/resources/webhooks.go +++ b/internal/provider/resources/webhooks.go @@ -115,14 +115,20 @@ 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", }, @@ -133,7 +139,7 @@ func (r *WebhookResource) Schema(_ context.Context, _ resource.SchemaRequest, re "service_account_id": schema.StringAttribute{ Optional: true, CustomType: customtypes.UUIDType{}, - Description: "ID of the Service Account to which this webhook belongs. If set, the webhook request will be authorized with the Service Account's API key.", + 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.", }, }, } @@ -142,8 +148,8 @@ func (r *WebhookResource) Schema(_ context.Context, _ resource.SchemaRequest, re // copyWebhookResponseToModel maps an API response to a model that is saved in Terraform state. func copyWebhookResponseToModel(webhook *api.Webhook, tfModel *WebhookResourceModel, endpointHost string) { 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) @@ -151,6 +157,7 @@ func copyWebhookResponseToModel(webhook *api.Webhook, tfModel *WebhookResourceMo tfModel.AccountID = customtypes.NewUUIDValue(webhook.AccountID) tfModel.WorkspaceID = customtypes.NewUUIDValue(webhook.WorkspaceID) tfModel.Endpoint = types.StringValue(fmt.Sprintf("%s/hooks/%s", endpointHost, webhook.Slug)) + tfModel.ServiceAccountID = customtypes.NewUUIDPointerValue(webhook.ServiceAccountID) } // Create creates the resource and sets the initial Terraform state. @@ -170,10 +177,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) @@ -261,10 +271,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) diff --git a/internal/provider/resources/webhooks_test.go b/internal/provider/resources/webhooks_test.go index b303fd0..52cbc8e 100644 --- a/internal/provider/resources/webhooks_test.go +++ b/internal/provider/resources/webhooks_test.go @@ -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", @@ -92,6 +111,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, From ab275fe299881581f5a83fe55af1d49733e676e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 6 Jan 2025 19:48:10 +0000 Subject: [PATCH 3/3] Generate Terraform Docs --- docs/data-sources/webhook.md | 1 + docs/resources/webhook.md | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/data-sources/webhook.md b/docs/data-sources/webhook.md index f0e2d29..9058e2d 100644 --- a/docs/data-sources/webhook.md +++ b/docs/data-sources/webhook.md @@ -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) diff --git a/docs/resources/webhook.md b/docs/resources/webhook.md index 667178a..ad56852 100644 --- a/docs/resources/webhook.md +++ b/docs/resources/webhook.md @@ -27,7 +27,7 @@ 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" @@ -35,9 +35,27 @@ resource "prefect_webhook" "example_with_file" { 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 + } } ``` @@ -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