From cdad1cbf74c6b47621e26d132fd781fbc9c926c3 Mon Sep 17 00:00:00 2001 From: armalite Date: Wed, 28 Aug 2024 19:38:35 +1200 Subject: [PATCH 1/3] Add base api payloads and client for webhooks --- go.mod | 2 +- internal/api/webhooks.go | 56 ++++++++++++++ internal/client/webhooks.go | 145 ++++++++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 internal/api/webhooks.go create mode 100644 internal/client/webhooks.go diff --git a/go.mod b/go.mod index 6d8aceb..e0d44cd 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/prefecthq/terraform-provider-prefect go 1.21 toolchain go1.22.1 - +replace github.com/prefecthq/terraform-provider-prefect => /Users/adeeb.rahman/Documents/dev/personal/terraform-provider-prefect require ( github.com/avast/retry-go/v4 v4.6.0 github.com/go-test/deep v1.1.1 diff --git a/internal/api/webhooks.go b/internal/api/webhooks.go new file mode 100644 index 0000000..1f2a9ba --- /dev/null +++ b/internal/api/webhooks.go @@ -0,0 +1,56 @@ +package api + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +type WebhooksClient interface { + Create(ctx context.Context, accountID, workspaceID string, request WebhookCreateRequest) (*Webhook, error) + Get(ctx context.Context, accountID, workspaceID, webhookID string) (*Webhook, error) + Update(ctx context.Context, accountID, workspaceID, webhookID string, request WebhookUpdateRequest) error + Delete(ctx context.Context, accountID, workspaceID, 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"` +} diff --git a/internal/client/webhooks.go b/internal/client/webhooks.go new file mode 100644 index 0000000..14ea8c7 --- /dev/null +++ b/internal/client/webhooks.go @@ -0,0 +1,145 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/google/uuid" + "github.com/prefecthq/terraform-provider-prefect/internal/api" +) + +type WebhooksClient struct { + hc *http.Client + apiKey string + routePrefix string +} + +func (c *Client) Webhooks(accountID, workspaceID uuid.UUID) (api.WebhooksClient, error) { + if c.apiKey == "" { + return nil, fmt.Errorf("apiKey is not set") + } + + if c.endpoint == "" { + return nil, fmt.Errorf("endpoint is not set") + } + + routePrefix := fmt.Sprintf("%s/api/accounts/%s/workspaces/%s/webhooks", c.endpoint, accountID, workspaceID) + + return &WebhooksClient{ + hc: c.hc, + apiKey: c.apiKey, + routePrefix: routePrefix, + }, nil +} + +func (wc *WebhooksClient) Create(ctx context.Context, accountID, workspaceID string, request api.WebhookCreateRequest) (*api.Webhook, error) { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(&request); err != nil { + return nil, fmt.Errorf("failed to encode request data: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, wc.routePrefix+"/", &buf) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + setDefaultHeaders(req, wc.apiKey) + + resp, err := wc.hc.Do(req) + if err != nil { + return nil, fmt.Errorf("http error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + errorBody, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("status code %s, error=%s", resp.Status, errorBody) + } + + var response api.Webhook + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &response, nil +} + +func (wc *WebhooksClient) Get(ctx context.Context, accountID, workspaceID, webhookID string) (*api.Webhook, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, wc.routePrefix+"/"+webhookID, http.NoBody) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + setDefaultHeaders(req, wc.apiKey) + + resp, err := wc.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 response api.Webhook + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &response, nil +} + +func (wc *WebhooksClient) Update(ctx context.Context, accountID, workspaceID, webhookID string, request api.WebhookUpdateRequest) error { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(&request); err != nil { + return fmt.Errorf("failed to encode request data: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, wc.routePrefix+"/"+webhookID, &buf) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + setDefaultHeaders(req, wc.apiKey) + + resp, err := wc.hc.Do(req) + if err != nil { + return fmt.Errorf("http error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + errorBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("status code %s, error=%s", resp.Status, errorBody) + } + + return nil +} + +func (wc *WebhooksClient) Delete(ctx context.Context, accountID, workspaceID, webhookID string) error { + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, wc.routePrefix+"/"+webhookID, http.NoBody) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + setDefaultHeaders(req, wc.apiKey) + + resp, err := wc.hc.Do(req) + if err != nil { + return fmt.Errorf("http error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + errorBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("status code %s, error=%s", resp.Status, errorBody) + } + + return nil +} From 7af7b85b5dde8e6fa9774707f93ec41e93c59018 Mon Sep 17 00:00:00 2001 From: armalite Date: Wed, 28 Aug 2024 20:04:42 +1200 Subject: [PATCH 2/3] Temporary directive --- go.mod | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e0d44cd..74cbe15 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/prefecthq/terraform-provider-prefect go 1.21 toolchain go1.22.1 -replace github.com/prefecthq/terraform-provider-prefect => /Users/adeeb.rahman/Documents/dev/personal/terraform-provider-prefect + require ( github.com/avast/retry-go/v4 v4.6.0 github.com/go-test/deep v1.1.1 @@ -88,3 +88,5 @@ require ( gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/prefecthq/terraform-provider-prefect => /Users/adeeb.rahman/Documents/dev/personal/terraform-provider-prefect \ No newline at end of file From 78b57415f9084a4d7a861fba8ba122ff80495e41 Mon Sep 17 00:00:00 2001 From: armalite Date: Wed, 28 Aug 2024 20:41:41 +1200 Subject: [PATCH 3/3] Add first iteration of webhook resource and datasource implementation --- internal/api/client.go | 1 + internal/api/webhooks.go | 9 +- internal/client/webhooks.go | 39 ++- internal/provider/datasources/webhooks.go | 210 +++++++++++++++ internal/provider/resources/webhooks.go | 308 ++++++++++++++++++++++ 5 files changed, 557 insertions(+), 10 deletions(-) create mode 100644 internal/provider/datasources/webhooks.go create mode 100644 internal/provider/resources/webhooks.go diff --git a/internal/api/client.go b/internal/api/client.go index 3e998da..c892fc0 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -22,4 +22,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) } diff --git a/internal/api/webhooks.go b/internal/api/webhooks.go index 1f2a9ba..9662aea 100644 --- a/internal/api/webhooks.go +++ b/internal/api/webhooks.go @@ -8,10 +8,11 @@ import ( ) type WebhooksClient interface { - Create(ctx context.Context, accountID, workspaceID string, request WebhookCreateRequest) (*Webhook, error) - Get(ctx context.Context, accountID, workspaceID, webhookID string) (*Webhook, error) - Update(ctx context.Context, accountID, workspaceID, webhookID string, request WebhookUpdateRequest) error - Delete(ctx context.Context, accountID, workspaceID, webhookID string) error + Create(ctx context.Context, accountID, workspaceID string, request WebhookCreateRequest) (*Webhook, error) + Get(ctx context.Context, accountID, workspaceID, webhookID string) (*Webhook, error) + List(ctx context.Context, accountID, workspaceID string) ([]*Webhook, error) + Update(ctx context.Context, accountID, workspaceID, webhookID string, request WebhookUpdateRequest) error + Delete(ctx context.Context, accountID, workspaceID, webhookID string) error } /*** REQUEST DATA STRUCTS ***/ diff --git a/internal/client/webhooks.go b/internal/client/webhooks.go index 14ea8c7..0611c44 100644 --- a/internal/client/webhooks.go +++ b/internal/client/webhooks.go @@ -12,7 +12,7 @@ import ( "github.com/prefecthq/terraform-provider-prefect/internal/api" ) -type WebhooksClient struct { +type webhooksClient struct { hc *http.Client apiKey string routePrefix string @@ -29,14 +29,14 @@ func (c *Client) Webhooks(accountID, workspaceID uuid.UUID) (api.WebhooksClient, routePrefix := fmt.Sprintf("%s/api/accounts/%s/workspaces/%s/webhooks", c.endpoint, accountID, workspaceID) - return &WebhooksClient{ + return &webhooksClient{ hc: c.hc, apiKey: c.apiKey, routePrefix: routePrefix, }, nil } -func (wc *WebhooksClient) Create(ctx context.Context, accountID, workspaceID string, request api.WebhookCreateRequest) (*api.Webhook, error) { +func (wc *webhooksClient) Create(ctx context.Context, accountID, workspaceID string, request api.WebhookCreateRequest) (*api.Webhook, error) { var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(&request); err != nil { return nil, fmt.Errorf("failed to encode request data: %w", err) @@ -68,7 +68,7 @@ func (wc *WebhooksClient) Create(ctx context.Context, accountID, workspaceID str return &response, nil } -func (wc *WebhooksClient) Get(ctx context.Context, accountID, workspaceID, webhookID string) (*api.Webhook, error) { +func (wc *webhooksClient) Get(ctx context.Context, accountID, workspaceID, webhookID string) (*api.Webhook, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, wc.routePrefix+"/"+webhookID, http.NoBody) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) @@ -95,7 +95,7 @@ func (wc *WebhooksClient) Get(ctx context.Context, accountID, workspaceID, webho return &response, nil } -func (wc *WebhooksClient) Update(ctx context.Context, accountID, workspaceID, webhookID string, request api.WebhookUpdateRequest) error { +func (wc *webhooksClient) Update(ctx context.Context, accountID, workspaceID, webhookID string, request api.WebhookUpdateRequest) error { var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(&request); err != nil { return fmt.Errorf("failed to encode request data: %w", err) @@ -122,7 +122,7 @@ func (wc *WebhooksClient) Update(ctx context.Context, accountID, workspaceID, we return nil } -func (wc *WebhooksClient) Delete(ctx context.Context, accountID, workspaceID, webhookID string) error { +func (wc *webhooksClient) Delete(ctx context.Context, accountID, workspaceID, webhookID string) error { req, err := http.NewRequestWithContext(ctx, http.MethodDelete, wc.routePrefix+"/"+webhookID, http.NoBody) if err != nil { return fmt.Errorf("error creating request: %w", err) @@ -143,3 +143,30 @@ func (wc *WebhooksClient) Delete(ctx context.Context, accountID, workspaceID, we return nil } + +func (w *webhooksClient) List(ctx context.Context, accountID, workspaceID string) ([]*api.Webhook, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/", w.routePrefix), nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + + setDefaultHeaders(req, w.apiKey) + + resp, err := w.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 webhooks []*api.Webhook + if err := json.NewDecoder(resp.Body).Decode(&webhooks); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return webhooks, nil +} \ No newline at end of file diff --git a/internal/provider/datasources/webhooks.go b/internal/provider/datasources/webhooks.go new file mode 100644 index 0000000..e4cb0df --- /dev/null +++ b/internal/provider/datasources/webhooks.go @@ -0,0 +1,210 @@ +package datasources + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "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" +) + +var _ = datasource.DataSourceWithConfigure(&WebhookDataSource{}) + +// WebhookDataSource contains state for the data source. +type WebhookDataSource struct { + client api.PrefectClient +} + +// 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"` +} + +// NewWebhookDataSource returns a new WebhookDataSource. +func NewWebhookDataSource() datasource.DataSource { + return &WebhookDataSource{} +} + +// Metadata returns the data source type name. +func (d *WebhookDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_webhook" +} + +// Configure initializes runtime state for the data source. +func (d *WebhookDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(api.PrefectClient) + if !ok { + resp.Diagnostics.Append(helpers.ConfigureTypeErrorDiagnostic("data source", req.ProviderData)) + + return + } + + d.client = client +} + +var webhookAttributes = map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Optional: true, + CustomType: customtypes.UUIDType{}, + Description: "Webhook ID (UUID)", + }, + "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)", + }, + "name": schema.StringAttribute{ + Computed: true, + Optional: true, + Description: "Name of the webhook", + }, + "description": schema.StringAttribute{ + Computed: true, + Description: "Description of the webhook", + }, + "enabled": schema.BoolAttribute{ + Computed: true, + Description: "Whether the webhook is enabled", + }, + "template": schema.StringAttribute{ + Computed: true, + Description: "Template used by the webhook", + }, + "account_id": schema.StringAttribute{ + CustomType: customtypes.UUIDType{}, + Description: "Account ID (UUID)", + Optional: true, + }, + "workspace_id": schema.StringAttribute{ + CustomType: customtypes.UUIDType{}, + Description: "Workspace ID (UUID)", + Optional: true, + }, + "slug": schema.StringAttribute{ + Computed: true, + Description: "Slug of the webhook", + }, +} + +// Schema defines the schema for the data source. +func (d *WebhookDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + 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. +`, + Attributes: webhookAttributes, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *WebhookDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var model WebhookDataSourceModel + + // Populate the model from data source configuration and emit diagnostics on error + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + if model.ID.IsNull() && model.Name.IsNull() { + resp.Diagnostics.AddError( + "Both ID and Name are unset", + "Either a Webhook ID or Name is required to read a Webhook.", + ) + + return + } + + client, err := d.client.Webhooks(model.AccountID.ValueUUID(), model.WorkspaceID.ValueUUID()) + if err != nil { + resp.Diagnostics.Append(helpers.CreateClientErrorDiagnostic("Webhook", err)) + + return + } + + // A Webhook can be read by either ID or Name. + // If both are set, we prefer the ID + var webhook *api.Webhook + var operation string + if !model.ID.IsNull() { + operation = "get" + webhook, err = client.Get(ctx, model.AccountID.ValueString(), model.WorkspaceID.ValueString(), model.ID.ValueString()) + } else if !model.Name.IsNull() { + var webhooks []*api.Webhook + operation = "list" + webhooks, err = client.List(ctx, model.AccountID.ValueString(), model.WorkspaceID.ValueString()) + + // The error from the API call should take precedence + // followed by this custom error if a specific webhook is not returned + if err == nil { + for _, wh := range webhooks { + if wh.Name == model.Name.ValueString() { + webhook = wh + break + } + } + + if webhook == nil { + err = fmt.Errorf("a Webhook with the name=%s could not be found", model.Name.ValueString()) + } + } + } + + if webhook == nil { + resp.Diagnostics.AddError( + "Error refreshing Webhook state", + fmt.Sprintf("Could not find Webhook with ID=%s and Name=%s", model.ID.ValueString(), model.Name.ValueString()), + ) + + return + } + + if err != nil { + resp.Diagnostics.Append(helpers.ResourceClientErrorDiagnostic("Webhook", operation, err)) + + return + } + + model.ID = customtypes.NewUUIDValue(webhook.ID) + model.Created = customtypes.NewTimestampPointerValue(&webhook.Created) + model.Updated = customtypes.NewTimestampPointerValue(&webhook.Updated) + + model.Name = types.StringValue(webhook.Name) + model.Description = types.StringValue(webhook.Description) + model.Enabled = types.BoolValue(webhook.Enabled) + model.Template = types.StringValue(webhook.Template) + model.AccountID = customtypes.NewUUIDValue(webhook.AccountID) + model.WorkspaceID = customtypes.NewUUIDValue(webhook.WorkspaceID) + model.Slug = types.StringValue(webhook.Slug) + + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/resources/webhooks.go b/internal/provider/resources/webhooks.go new file mode 100644 index 0000000..2770a20 --- /dev/null +++ b/internal/provider/resources/webhooks.go @@ -0,0 +1,308 @@ +package resources + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "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" +) + +var ( + _ = resource.ResourceWithConfigure(&WebhookResource{}) + _ = resource.ResourceWithImportState(&WebhookResource{}) +) + +type WebhookResource struct { + client api.PrefectClient +} + +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"` + Slug types.String `tfsdk:"slug"` +} + +// NewWebhookResource returns a new WebhookResource. +func NewWebhookResource() resource.Resource { + return &WebhookResource{} +} + +func (r *WebhookResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_webhook" +} + +func (r *WebhookResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(api.PrefectClient) + if !ok { + resp.Diagnostics.Append(helpers.ConfigureTypeErrorDiagnostic("resource", req.ProviderData)) + + return + } + + r.client = client +} + +func (r *WebhookResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "The resource `webhook` represents a Prefect Cloud Webhook. " + + "Webhooks allow external services to trigger events in Prefect.", + Version: 1, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Description: "Webhook ID (UUID)", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Required: true, + Description: "Name of the webhook", + }, + "description": schema.StringAttribute{ + Optional: true, + Description: "Description of the webhook", + }, + "enabled": schema.BoolAttribute{ + Required: true, + Description: "Whether the webhook is enabled", + }, + "template": schema.StringAttribute{ + Required: true, + Description: "Template used by the webhook", + }, + "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{ + Required: true, + CustomType: customtypes.UUIDType{}, + Description: "Account ID (UUID)", + }, + "workspace_id": schema.StringAttribute{ + Required: true, + CustomType: customtypes.UUIDType{}, + Description: "Workspace ID (UUID)", + }, + "slug": schema.StringAttribute{ + Computed: true, + Description: "Slug of the webhook", + }, + }, + } +} + +// 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.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.Slug = types.StringValue(webhook.Slug) +} + +// Create creates the resource and sets the initial Terraform state. +func (r *WebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan WebhookResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + webhookClient, err := r.client.Webhooks(plan.AccountID.ValueUUID(), plan.WorkspaceID.ValueUUID()) + if err != nil { + resp.Diagnostics.Append(helpers.CreateClientErrorDiagnostic("Webhook", err)) + + return + } + + createReq := api.WebhookCreateRequest{ + Name: plan.Name.ValueString(), + Description: plan.Description.ValueString(), + Enabled: plan.Enabled.ValueBool(), + Template: plan.Template.ValueString(), + } + + webhook, err := webhookClient.Create(ctx, plan.AccountID.ValueString(), plan.WorkspaceID.ValueString(), createReq) + if err != nil { + resp.Diagnostics.Append(helpers.ResourceClientErrorDiagnostic("Webhook", "create", err)) + + return + } + + copyWebhookResponseToModel(webhook, &plan) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *WebhookResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state WebhookResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if state.ID.IsNull() && state.Name.IsNull() { + resp.Diagnostics.AddError( + "Both ID and Name are unset", + "This is a bug in the Terraform provider. Please report it to the maintainers.", + ) + + return + } + + client, err := r.client.Webhooks(state.AccountID.ValueUUID(), state.WorkspaceID.ValueUUID()) + if err != nil { + resp.Diagnostics.Append(helpers.CreateClientErrorDiagnostic("Webhook", err)) + + return + } + + var webhook *api.Webhook + if !state.ID.IsNull() { + webhook, err = client.Get(ctx, state.AccountID.ValueString(), state.WorkspaceID.ValueString(), state.ID.ValueString()) + } else { + resp.Diagnostics.AddError( + "ID is unset", + "Webhook ID must be set to retrieve the resource.", + ) + return + } + + if err != nil { + resp.Diagnostics.Append(helpers.ResourceClientErrorDiagnostic("Webhook", "get", err)) + + return + } + + copyWebhookResponseToModel(webhook, &state) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *WebhookResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan WebhookResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + var state WebhookResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.Webhooks(plan.AccountID.ValueUUID(), plan.WorkspaceID.ValueUUID()) + if err != nil { + resp.Diagnostics.Append(helpers.CreateClientErrorDiagnostic("Webhook", err)) + + return + } + + updateReq := api.WebhookUpdateRequest{ + Name: plan.Name.ValueString(), + Description: plan.Description.ValueString(), + Enabled: plan.Enabled.ValueBool(), + Template: plan.Template.ValueString(), + } + + err = client.Update(ctx, plan.AccountID.ValueString(), plan.WorkspaceID.ValueString(), state.ID.ValueString(), updateReq) + if err != nil { + resp.Diagnostics.Append(helpers.ResourceClientErrorDiagnostic("Webhook", "update", err)) + + return + } + + webhook, err := client.Get(ctx, plan.AccountID.ValueString(), plan.WorkspaceID.ValueString(), state.ID.ValueString()) + if err != nil { + resp.Diagnostics.Append(helpers.ResourceClientErrorDiagnostic("Webhook", "get", err)) + + return + } + + copyWebhookResponseToModel(webhook, &plan) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *WebhookResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state WebhookResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + client, err := r.client.Webhooks(state.AccountID.ValueUUID(), state.WorkspaceID.ValueUUID()) + if err != nil { + resp.Diagnostics.Append(helpers.CreateClientErrorDiagnostic("Webhook", err)) + + return + } + + err = client.Delete(ctx, state.AccountID.ValueString(), state.WorkspaceID.ValueString(), state.ID.ValueString()) + if err != nil { + resp.Diagnostics.Append(helpers.ResourceClientErrorDiagnostic("Webhook", "delete", err)) + + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } +} + +// ImportState imports the resource into Terraform state. +func (r *WebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + if strings.HasPrefix(req.ID, "name/") { + name := strings.TrimPrefix(req.ID, "name/") + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), name)...) + } else { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + } +}