Skip to content

Commit

Permalink
add endpoint host and hook into webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
parkedwards committed Dec 24, 2024
1 parent 8ad5d78 commit 527116d
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 18 deletions.
2 changes: 2 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import "github.com/google/uuid"
//
//nolint:interfacebloat // we'll accept a larger PrefectClient interface
type PrefectClient interface {
GetEndpointHost() string

Accounts(accountID uuid.UUID) (AccountsClient, error)
Automations(accountID uuid.UUID, workspaceID uuid.UUID) (AutomationsClient, error)
AccountMemberships(accountID uuid.UUID) (AccountMembershipsClient, error)
Expand Down
3 changes: 2 additions & 1 deletion internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func New(opts ...Option) (*Client, error) {

// WithEndpoint configures the client to communicate with a self-hosted
// Prefect server or Prefect Cloud.
func WithEndpoint(endpoint string) Option {
func WithEndpoint(endpoint string, host string) Option {
return func(client *Client) error {
_, err := url.Parse(endpoint)
if err != nil {
Expand All @@ -75,6 +75,7 @@ func WithEndpoint(endpoint string) Option {
}

client.endpoint = endpoint
client.endpointHost = host

return nil
}
Expand Down
7 changes: 7 additions & 0 deletions internal/client/getters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package client

// GetEndpointHost returns the endpoint host.
// eg. https://api.prefect.cloud
func (c *Client) GetEndpointHost() string {
return c.endpointHost
}
1 change: 1 addition & 0 deletions internal/client/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
type Client struct {
hc *http.Client
endpoint string
endpointHost string
apiKey string
defaultAccountID uuid.UUID
defaultWorkspaceID uuid.UUID
Expand Down
9 changes: 8 additions & 1 deletion internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ func (p *PrefectProvider) Configure(ctx context.Context, req provider.ConfigureR
}
isPrefectCloudEndpoint := helpers.IsCloudEndpoint(endpointURL.Host)

// Extracts the host (without the /api suffix),
// so we can store it on the Client object in addition to the endpoint.
// For non-Cloud endpoints, it will likely be the same as .endpoint.
// This is useful for certain resources where we need access to the
// endpoint host to construct custom URLs as a resource attribute.
endpointHost := fmt.Sprintf("%s://%s", endpointURL.Scheme, endpointURL.Host)

// Extract the API Key from configuration or environment variable.
var apiKey string
if !config.APIKey.IsNull() {
Expand Down Expand Up @@ -201,7 +208,7 @@ func (p *PrefectProvider) Configure(ctx context.Context, req provider.ConfigureR
tflog.Debug(ctx, "Creating Prefect client")

prefectClient, err := client.New(
client.WithEndpoint(endpoint),
client.WithEndpoint(endpoint, endpointHost),
client.WithAPIKey(apiKey),
client.WithDefaults(accountID, config.WorkspaceID.ValueUUID()),
)
Expand Down
18 changes: 5 additions & 13 deletions internal/provider/resources/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ 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) {
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)
Expand All @@ -144,6 +144,7 @@ func copyWebhookResponseToModel(webhook *api.Webhook, tfModel *WebhookResourceMo
tfModel.Template = types.StringValue(webhook.Template)
tfModel.AccountID = customtypes.NewUUIDValue(webhook.AccountID)
tfModel.WorkspaceID = customtypes.NewUUIDValue(webhook.WorkspaceID)
tfModel.Endpoint = types.StringValue(fmt.Sprintf("%s/hooks/%s", endpointHost, webhook.Slug))
}

// Create creates the resource and sets the initial Terraform state.
Expand Down Expand Up @@ -176,10 +177,7 @@ func (r *WebhookResource) Create(ctx context.Context, req resource.CreateRequest
return
}

// Extract the endpoint from the provider configuration.
// https://github.com/PrefectHQ/terraform-provider-prefect/issues/333
copyWebhookResponseToModel(webhook, &plan)
plan.Endpoint = types.StringValue(fmt.Sprintf("https://api.prefect.cloud/hooks/%s", webhook.Slug))
copyWebhookResponseToModel(webhook, &plan, r.client.GetEndpointHost())

resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
if resp.Diagnostics.HasError() {
Expand Down Expand Up @@ -230,10 +228,7 @@ func (r *WebhookResource) Read(ctx context.Context, req resource.ReadRequest, re
return
}

// Extract the endpoint from the provider configuration.
// https://github.com/PrefectHQ/terraform-provider-prefect/issues/333
copyWebhookResponseToModel(webhook, &state)
state.Endpoint = types.StringValue(fmt.Sprintf("https://api.prefect.cloud/hooks/%s", webhook.Slug))
copyWebhookResponseToModel(webhook, &state, r.client.GetEndpointHost())

resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
if resp.Diagnostics.HasError() {
Expand Down Expand Up @@ -280,10 +275,7 @@ func (r *WebhookResource) Update(ctx context.Context, req resource.UpdateRequest
return
}

// Extract the endpoint from the provider configuration.
// https://github.com/PrefectHQ/terraform-provider-prefect/issues/333
copyWebhookResponseToModel(webhook, &plan)
plan.Endpoint = types.StringValue(fmt.Sprintf("https://api.prefect.cloud/hooks/%s", webhook.Slug))
copyWebhookResponseToModel(webhook, &plan, r.client.GetEndpointHost())

resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
if resp.Diagnostics.HasError() {
Expand Down
24 changes: 22 additions & 2 deletions internal/provider/resources/webhooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,21 @@ func TestAccResource_webhook(t *testing.T) {
PreCheck: func() { testutils.AccTestPreCheck(t) },
Steps: []resource.TestStep{
{
// Check creation + existence of the work pool resource
// Check creation + existence of the webhook resource
Config: fixtureAccWebhook(workspace.Resource, randomName, webhookTemplateDynamic, true),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckWebhookExists(webhookResourceName, &webhook),
testAccCheckWebhookEndpoint(webhookResourceName, &webhook),
resource.TestCheckResourceAttr(webhookResourceName, "name", randomName),
resource.TestCheckResourceAttr(webhookResourceName, "enabled", "true"),
),
},
{
// Check that changing the paused state will update the resource in place
// Check that changing the enabled state will update the resource in place
Config: fixtureAccWebhook(workspace.Resource, randomName, webhookTemplateDynamic, false),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckWebhookExists(webhookResourceName, &webhook),
testAccCheckWebhookEndpoint(webhookResourceName, &webhook),
resource.TestCheckResourceAttr(webhookResourceName, "name", randomName),
resource.TestCheckResourceAttr(webhookResourceName, "enabled", "false"),
),
Expand All @@ -85,6 +87,7 @@ func TestAccResource_webhook(t *testing.T) {
Config: fixtureAccWebhook(workspace.Resource, randomName, webhookTemplateStatic, true),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckWebhookExists(webhookResourceName, &webhook),
testAccCheckWebhookEndpoint(webhookResourceName, &webhook),
resource.TestCheckResourceAttr(webhookResourceName, "name", randomName),
resource.TestCheckResourceAttr(webhookResourceName, "enabled", "true"),
),
Expand Down Expand Up @@ -131,6 +134,23 @@ func testAccCheckWebhookExists(webhookResourceName string, webhook *api.Webhook)
}
}

func testAccCheckWebhookEndpoint(webhookResourceName string, webhook *api.Webhook) resource.TestCheckFunc {
return func(state *terraform.State) error {
webhookResource, exists := state.RootModule().Resources[webhookResourceName]
if !exists {
return fmt.Errorf("Resource not found in state: %s", webhookResourceName)
}

storedEndpoint := webhookResource.Primary.Attributes["endpoint"]
expectedEndpoint := fmt.Sprintf("https://api.stg.prefect.dev/hooks/%s", webhook.Slug)
if storedEndpoint != expectedEndpoint {
return fmt.Errorf("Endpoint does not match expected value: %s != %s", storedEndpoint, expectedEndpoint)
}

return nil
}
}

func getWebhookImportStateID(webhookResourceName string) resource.ImportStateIdFunc {
return func(state *terraform.State) (string, error) {
workspaceResource, exists := state.RootModule().Resources[testutils.WorkspaceResourceName]
Expand Down
6 changes: 5 additions & 1 deletion internal/testutils/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package testutils

import (
"fmt"
"net/url"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -74,8 +75,11 @@ func NewTestClient() (api.PrefectClient, error) {
endpoint = fmt.Sprintf("%s/api", endpoint)
}

endpointURL, _ := url.Parse(endpoint)
endpointHost := fmt.Sprintf("%s://%s", endpointURL.Scheme, endpointURL.Host)

prefectClient, _ := client.New(
client.WithEndpoint(endpoint),
client.WithEndpoint(endpoint, endpointHost),
client.WithAPIKey(apiKey),
client.WithDefaults(accountID, uuid.Nil),
)
Expand Down

0 comments on commit 527116d

Please sign in to comment.