diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 077e2529..01bc4a96 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-@talsabagport @danielsinai @dvirsegev @matarpeles
\ No newline at end of file
+@talsabagport @danielsinai @dvirsegev @matarpeles @pazhersh @MPTG94
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 58359ca4..a1fe66ac 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -22,4 +22,3 @@ description: |-
### Optional
- `base_url` (String) Base URL for Port-labs (environment: `PORT_BASE_URL`)
-- `token` (String, Sensitive) Token for Port-labs
diff --git a/docs/resources/port_webhook.md b/docs/resources/port_webhook.md
new file mode 100644
index 00000000..afbd2d35
--- /dev/null
+++ b/docs/resources/port_webhook.md
@@ -0,0 +1,77 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "port_webhook Resource - terraform-provider-port-labs"
+subcategory: ""
+description: |-
+ Webhook resource
+---
+
+# port_webhook (Resource)
+
+Webhook resource
+
+
+
+
+## Schema
+
+### Optional
+
+- `description` (String) The description of the webhook
+- `enabled` (Boolean) Whether the webhook is enabled
+- `icon` (String) The icon of the webhook
+- `identifier` (String) The identifier of the webhook
+- `mappings` (Attributes List) The mappings of the webhook (see [below for nested schema](#nestedatt--mappings))
+- `security` (Attributes) The security of the webhook (see [below for nested schema](#nestedatt--security))
+- `title` (String) The title of the webhook
+
+### Read-Only
+
+- `created_at` (String) The creation date of the webhook
+- `created_by` (String) The creator of the webhook
+- `id` (String) The ID of this resource.
+- `updated_at` (String) The last update date of the webhook
+- `updated_by` (String) The last updater of the webhook
+
+
+### Nested Schema for `mappings`
+
+Required:
+
+- `blueprint` (String) The blueprint of the mapping
+- `entity` (Attributes) The entity of the mapping (see [below for nested schema](#nestedatt--mappings--entity))
+
+Optional:
+
+- `filter` (String) The filter of the mapping
+- `items_to_parse` (String) The items to parser of the mapping
+
+
+### Nested Schema for `mappings.entity`
+
+Required:
+
+- `identifier` (String) The identifier of the entity
+
+Optional:
+
+- `icon` (String) The icon of the entity
+- `properties` (Map of String) The properties of the entity
+- `relations` (Map of String) The relations of the entity
+- `team` (String) The team of the entity
+- `title` (String) The title of the entity
+
+
+
+
+### Nested Schema for `security`
+
+Optional:
+
+- `request_identifier_path` (String) The request identifier path of the webhook
+- `secret` (String) The secret of the webhook
+- `signature_algorithm` (String) The signature algorithm of the webhook
+- `signature_header_name` (String) The signature header name of the webhook
+- `signature_prefix` (String) The signature prefix of the webhook
+
+
diff --git a/internal/cli/models.go b/internal/cli/models.go
index 4435f8c1..7da6d33a 100644
--- a/internal/cli/models.go
+++ b/internal/cli/models.go
@@ -195,13 +195,49 @@ type (
Required *bool `json:"required,omitempty"`
Many *bool `json:"many,omitempty"`
}
+
+ Webhook struct {
+ Meta
+ Identifier string `json:"identifier,omitempty"`
+ Title *string `json:"title,omitempty"`
+ Icon *string `json:"icon,omitempty"`
+ Description *string `json:"description,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
+ Security *Security `json:"security,omitempty"`
+ Mappings []Mappings `json:"mappings,omitempty"`
+ }
+
+ Security struct {
+ Secret *string `json:"secret,omitempty"`
+ SignatureHeaderName *string `json:"signatureHeaderName,omitempty"`
+ SignatureAlgorithm *string `json:"signatureAlgorithm,omitempty"`
+ SignaturePrefix *string `json:"signaturePrefix,omitempty"`
+ RequestIdentifierPath *string `json:"requestIdentifierPath,omitempty"`
+ }
+
+ EntityProperty struct {
+ Identifier string `json:"identifier,omitempty"`
+ Title *string `json:"title,omitempty"`
+ Icon *string `json:"icon,omitempty"`
+ Team *string `json:"team,omitempty"`
+ Properties map[string]string `json:"properties,omitempty"`
+ Relations map[string]string `json:"relations,omitempty"`
+ }
+
+ Mappings struct {
+ Blueprint string `json:"blueprint,omitempty"`
+ Filter *string `json:"filter,omitempty"`
+ ItemsToParse *string `json:"items_to_parse,omitempty"`
+ Entity *EntityProperty `json:"entity,omitempty"`
+ }
)
type PortBody struct {
- OK bool `json:"ok"`
- Entity Entity `json:"entity"`
- Blueprint Blueprint `json:"blueprint"`
- Action Action `json:"action"`
+ OK bool `json:"ok"`
+ Entity Entity `json:"entity"`
+ Blueprint Blueprint `json:"blueprint"`
+ Action Action `json:"action"`
+ Integration Webhook `json:"integration"`
}
type PortProviderModel struct {
diff --git a/internal/cli/webhook.go b/internal/cli/webhook.go
new file mode 100644
index 00000000..59560996
--- /dev/null
+++ b/internal/cli/webhook.go
@@ -0,0 +1,87 @@
+package cli
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+)
+
+func (c *PortClient) ReadWebhook(ctx context.Context, webhookID string) (*Webhook, int, error) {
+ pb := &PortBody{}
+ url := "v1/webhooks/{webhook_identifier}"
+ resp, err := c.Client.R().
+ SetContext(ctx).
+ SetHeader("Accept", "application/json").
+ SetResult(pb).
+ SetPathParam("webhook_identifier", webhookID).
+ Get(url)
+ if err != nil {
+ return nil, resp.StatusCode(), err
+ }
+ if !pb.OK {
+ return nil, resp.StatusCode(), fmt.Errorf("failed to read webhook, got: %s", resp.Body())
+ }
+ return &pb.Integration, resp.StatusCode(), nil
+}
+
+func (c *PortClient) CreateWebhook(ctx context.Context, webhook *Webhook) (*Webhook, error) {
+ url := "v1/webhooks"
+ resp, err := c.Client.R().
+ SetBody(webhook).
+ SetContext(ctx).
+ Post(url)
+ if err != nil {
+ return nil, err
+ }
+ var pb PortBody
+ err = json.Unmarshal(resp.Body(), &pb)
+ if err != nil {
+ return nil, err
+ }
+ if !pb.OK {
+ return nil, fmt.Errorf("failed to create webhook, got: %s", resp.Body())
+ }
+ return &pb.Integration, nil
+}
+
+func (c *PortClient) UpdateWebhook(ctx context.Context, webhookID string, webhook *Webhook) (*Webhook, error) {
+ url := "v1/webhooks/{webhook_identifier}"
+ resp, err := c.Client.R().
+ SetBody(webhook).
+ SetContext(ctx).
+ SetPathParam("webhook_identifier", webhookID).
+ Put(url)
+ if err != nil {
+ return nil, err
+ }
+ var pb PortBody
+ err = json.Unmarshal(resp.Body(), &pb)
+ if err != nil {
+ return nil, err
+ }
+ if !pb.OK {
+ return nil, fmt.Errorf("failed to create webhook, got: %s", resp.Body())
+ }
+ return &pb.Integration, nil
+}
+
+func (c *PortClient) DeleteWebhook(ctx context.Context, webhookID string) error {
+ url := "v1/webhooks/{webhook_identifier}"
+ resp, err := c.Client.R().
+ SetContext(ctx).
+ SetHeader("Accept", "application/json").
+ SetPathParam("webhook_identifier", webhookID).
+ Delete(url)
+ if err != nil {
+ return err
+ }
+ responseBody := make(map[string]interface{})
+ err = json.Unmarshal(resp.Body(), &responseBody)
+ if err != nil {
+ return err
+ }
+ if !(responseBody["ok"].(bool)) {
+ return fmt.Errorf("failed to delete webhook. got:\n%s", string(resp.Body()))
+ }
+ return nil
+}
diff --git a/port/webhook/model.go b/port/webhook/model.go
new file mode 100644
index 00000000..033d432a
--- /dev/null
+++ b/port/webhook/model.go
@@ -0,0 +1,44 @@
+package webhook
+
+import (
+ "github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type SecurityModel struct {
+ Secret types.String `tfsdk:"secret"`
+ SignatureHeaderName types.String `tfsdk:"signature_header_name"`
+ SignatureAlgorithm types.String `tfsdk:"signature_algorithm"`
+ SignaturePrefix types.String `tfsdk:"signature_prefix"`
+ RequestIdentifierPath types.String `tfsdk:"request_identifier_path"`
+}
+
+type EntityModel struct {
+ Identifier types.String `tfsdk:"identifier"`
+ Title types.String `tfsdk:"title"`
+ Icon types.String `tfsdk:"icon"`
+ Team types.String `tfsdk:"team"`
+ Properties map[string]string `tfsdk:"properties"`
+ Relations map[string]string `tfsdk:"relations"`
+}
+
+type MappingsModel struct {
+ Blueprint types.String `tfsdk:"blueprint"`
+ Filter types.String `tfsdk:"filter"`
+ ItemsToParse types.String `tfsdk:"items_to_parse"`
+ Entity *EntityModel `tfsdk:"entity"`
+}
+
+type WebhookModel struct {
+ ID types.String `tfsdk:"id"`
+ Icon types.String `tfsdk:"icon"`
+ Identifier types.String `tfsdk:"identifier"`
+ Title types.String `tfsdk:"title"`
+ Description types.String `tfsdk:"description"`
+ Enabled types.Bool `tfsdk:"enabled"`
+ Security *SecurityModel `tfsdk:"security"`
+ Mappings []MappingsModel `tfsdk:"mappings"`
+ CreatedAt types.String `tfsdk:"created_at"`
+ CreatedBy types.String `tfsdk:"created_by"`
+ UpdatedAt types.String `tfsdk:"updated_at"`
+ UpdatedBy types.String `tfsdk:"updated_by"`
+}
diff --git a/port/webhook/refreshWebhookState.go b/port/webhook/refreshWebhookState.go
new file mode 100644
index 00000000..d7e5b9c6
--- /dev/null
+++ b/port/webhook/refreshWebhookState.go
@@ -0,0 +1,67 @@
+package webhook
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+ "github.com/port-labs/terraform-provider-port-labs/internal/flex"
+)
+
+func refreshWebhookState(ctx context.Context, state *WebhookModel, w *cli.Webhook) error {
+ state.ID = types.StringValue(w.Identifier)
+ state.Identifier = types.StringValue(w.Identifier)
+ state.CreatedAt = types.StringValue(w.CreatedAt.String())
+ state.CreatedBy = types.StringValue(w.CreatedBy)
+ state.UpdatedAt = types.StringValue(w.UpdatedAt.String())
+ state.UpdatedBy = types.StringValue(w.UpdatedBy)
+ state.Icon = flex.GoStringToFramework(w.Icon)
+ state.Title = flex.GoStringToFramework(w.Title)
+ state.Description = flex.GoStringToFramework(w.Description)
+ state.Enabled = flex.GoBoolToFramework(w.Enabled)
+
+ if w.Security.RequestIdentifierPath != nil || w.Security.Secret != nil || w.Security.SignatureHeaderName != nil || w.Security.SignatureAlgorithm != nil || w.Security.SignaturePrefix != nil {
+ state.Security = &SecurityModel{
+ Secret: flex.GoStringToFramework(w.Security.Secret),
+ SignatureHeaderName: flex.GoStringToFramework(w.Security.SignatureHeaderName),
+ SignatureAlgorithm: flex.GoStringToFramework(w.Security.SignatureAlgorithm),
+ SignaturePrefix: flex.GoStringToFramework(w.Security.SignaturePrefix),
+ RequestIdentifierPath: flex.GoStringToFramework(w.Security.RequestIdentifierPath),
+ }
+ }
+
+ if len(w.Mappings) > 0 {
+ state.Mappings = []MappingsModel{}
+ for _, v := range w.Mappings {
+ mapping := &MappingsModel{
+ Blueprint: types.StringValue(v.Blueprint),
+ Entity: &EntityModel{
+ Identifier: types.StringValue(v.Entity.Identifier),
+ },
+ }
+
+ mapping.Filter = flex.GoStringToFramework(v.Filter)
+ mapping.ItemsToParse = flex.GoStringToFramework(v.ItemsToParse)
+ mapping.Entity.Icon = flex.GoStringToFramework(v.Entity.Icon)
+ mapping.Entity.Title = flex.GoStringToFramework(v.Entity.Title)
+ mapping.Entity.Team = flex.GoStringToFramework(v.Entity.Team)
+
+ if v.Entity.Properties != nil {
+ mapping.Entity.Properties = map[string]string{}
+ for k, v := range v.Entity.Properties {
+ mapping.Entity.Properties[k] = v
+ }
+ }
+
+ if v.Entity.Relations != nil {
+ mapping.Entity.Relations = map[string]string{}
+ for k, v := range v.Entity.Relations {
+ mapping.Entity.Relations[k] = v
+ }
+ }
+ state.Mappings = append(state.Mappings, *mapping)
+ }
+ }
+
+ return nil
+}
diff --git a/port/webhook/resource.go b/port/webhook/resource.go
new file mode 100644
index 00000000..e63bac69
--- /dev/null
+++ b/port/webhook/resource.go
@@ -0,0 +1,143 @@
+package webhook
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework/path"
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+)
+
+// Ensure provider defined types fully satisfy framework interfaces
+var _ resource.Resource = &WebhookResource{}
+var _ resource.ResourceWithImportState = &WebhookResource{}
+
+func NewWebhookResource() resource.Resource {
+ return &WebhookResource{}
+}
+
+type WebhookResource struct {
+ portClient *cli.PortClient
+}
+
+func (r *WebhookResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_webhook"
+}
+
+func (r *WebhookResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+
+ r.portClient = req.ProviderData.(*cli.PortClient)
+}
+
+func (r *WebhookResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state *WebhookModel
+
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ identifier := state.Identifier.ValueString()
+ w, statusCode, err := r.portClient.ReadWebhook(ctx, identifier)
+ if err != nil {
+ if statusCode == 404 {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ resp.Diagnostics.AddError("failed to read webhook", err.Error())
+ return
+ }
+
+ err = refreshWebhookState(ctx, state, w)
+ if err != nil {
+ resp.Diagnostics.AddError("failed writing webhook fields to resource", err.Error())
+ return
+ }
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *WebhookResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var state *WebhookModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ w, err := webhookResourceToPortBody(ctx, state)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to convert webhook resource to body", err.Error())
+ return
+ }
+
+ wp, err := r.portClient.CreateWebhook(ctx, w)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to create webhook", err.Error())
+ return
+ }
+
+ writeWebhookComputedFieldsToState(state, wp)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func writeWebhookComputedFieldsToState(state *WebhookModel, wp *cli.Webhook) {
+ state.ID = types.StringValue(wp.Identifier)
+ state.Identifier = types.StringValue(wp.Identifier)
+ state.CreatedAt = types.StringValue(wp.CreatedAt.String())
+ state.CreatedBy = types.StringValue(wp.CreatedBy)
+ state.UpdatedAt = types.StringValue(wp.UpdatedAt.String())
+ state.UpdatedBy = types.StringValue(wp.UpdatedBy)
+}
+
+func (r *WebhookResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var state *WebhookModel
+ resp.Diagnostics.Append(req.Plan.Get(ctx, &state)...)
+
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ w, err := webhookResourceToPortBody(ctx, state)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to convert webhook resource to body", err.Error())
+ return
+ }
+
+ wp, err := r.portClient.UpdateWebhook(ctx, w.Identifier, w)
+ if err != nil {
+ resp.Diagnostics.AddError("failed to update the webhook", err.Error())
+ return
+ }
+
+ writeWebhookComputedFieldsToState(state, wp)
+
+ resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
+}
+
+func (r *WebhookResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state *WebhookModel
+ resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.portClient.DeleteWebhook(ctx, state.Identifier.ValueString())
+
+ if err != nil {
+ resp.Diagnostics.AddError("failed to delete webhook", err.Error())
+ return
+ }
+
+}
+
+func (r *WebhookResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resp.Diagnostics.Append(resp.State.SetAttribute(
+ ctx, path.Root("identifier"), req.ID,
+ )...)
+}
diff --git a/port/webhook/resource_test.go b/port/webhook/resource_test.go
new file mode 100644
index 00000000..6447dabe
--- /dev/null
+++ b/port/webhook/resource_test.go
@@ -0,0 +1,206 @@
+package webhook_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/port-labs/terraform-provider-port-labs/internal/acctest"
+ "github.com/port-labs/terraform-provider-port-labs/internal/utils"
+)
+
+func testAccCreateBlueprintConfig(identifier string) string {
+ return fmt.Sprintf(`
+ resource "port_blueprint" "microservice" {
+ title = "TF test microservice"
+ icon = "Terraform"
+ identifier = "%s"
+ properties = {
+ string_props = {
+ "author" = {
+ type = "string"
+ title = "text"
+ }
+ "url" = {
+ type = "string"
+ title = "text"
+ }
+ }
+ }
+ }
+ `, identifier)
+}
+
+func TestAccPortWebhookBasic(t *testing.T) {
+ identifier := utils.GenID()
+ webhookIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
+ resource "port_webhook" "create_pr" {
+ identifier = "%s"
+ title = "Test"
+ icon = "Terraform"
+ enabled = true
+ }`, webhookIdentifier)
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "identifier", webhookIdentifier),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "title", "Test"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "icon", "Terraform"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "enabled", "true"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortWebhook(t *testing.T) {
+ identifier := utils.GenID()
+ webhookIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
+ resource "port_webhook" "create_pr" {
+ identifier = "%s"
+ title = "Test"
+ icon = "Terraform"
+ enabled = true
+ security = {
+ secret = "test"
+ signature_header_name = "X-Hub-Signature-256"
+ signature_algorithm = "sha256"
+ signature_prefix = "sha256="
+ request_identifier_path = "body.repository.full_name"
+ }
+ mappings = [
+ {
+ "blueprint" = "%s",
+ "filter" = ".headers.\"X-GitHub-Event\" == \"pull_request\"",
+ "entity" = {
+ "identifier" = ".body.pull_request.id | tostring",
+ "title" = ".body.pull_request.title",
+ "icon" = "Terraform",
+ "team" = "port",
+ "properties" = {
+ "author" = ".body.pull_request.user.login",
+ "url" = ".body.pull_request.html_url"
+ }
+ }
+ }
+ ]
+ lifecycle {
+ ignore_changes = [
+ security.secret
+ ]
+ }
+ depends_on = [
+ port_blueprint.microservice
+ ]
+ }`, webhookIdentifier, identifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "identifier", webhookIdentifier),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "title", "Test"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "icon", "Terraform"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "enabled", "true"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_header_name", "X-Hub-Signature-256"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_algorithm", "sha256"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_prefix", "sha256="),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "security.request_identifier_path", "body.repository.full_name"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.blueprint", identifier),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.filter", ".headers.\"X-GitHub-Event\" == \"pull_request\""),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.identifier", ".body.pull_request.id | tostring"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.title", ".body.pull_request.title"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.icon", "Terraform"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.team", "port"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.properties.author", ".body.pull_request.user.login"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.properties.url", ".body.pull_request.html_url"),
+ ),
+ },
+ },
+ })
+}
+
+func TestAccPortWebhookImport(t *testing.T) {
+ identifier := utils.GenID()
+ webhookIdentifier := utils.GenID()
+ var testAccActionConfigCreate = testAccCreateBlueprintConfig(identifier) + fmt.Sprintf(`
+ resource "port_webhook" "create_pr" {
+ identifier = "%s"
+ title = "Test"
+ icon = "Terraform"
+ enabled = true
+ security = {
+ signature_header_name = "X-Hub-Signature-256"
+ signature_algorithm = "sha256"
+ signature_prefix = "sha256="
+ request_identifier_path = "body.repository.full_name"
+ }
+ mappings = [
+ {
+ "blueprint" = "%s",
+ "filter" = ".headers.\"X-GitHub-Event\" == \"pull_request\"",
+ "entity" = {
+ "identifier" = ".body.pull_request.id | tostring",
+ "title" = ".body.pull_request.title",
+ "icon" = "Terraform",
+ "team" = "port",
+ "properties" = {
+ "author" = ".body.pull_request.user.login",
+ "url" = ".body.pull_request.html_url"
+ }
+ }
+ }
+ ]
+ lifecycle {
+ ignore_changes = [
+ security.secret
+ ]
+ }
+ depends_on = [
+ port_blueprint.microservice
+ ]
+ }`, webhookIdentifier, identifier)
+
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { acctest.TestAccPreCheck(t) },
+ ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: acctest.ProviderConfig + testAccActionConfigCreate,
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "identifier", webhookIdentifier),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "title", "Test"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "icon", "Terraform"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "enabled", "true"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_header_name", "X-Hub-Signature-256"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_algorithm", "sha256"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "security.signature_prefix", "sha256="),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "security.request_identifier_path", "body.repository.full_name"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.blueprint", identifier),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.filter", ".headers.\"X-GitHub-Event\" == \"pull_request\""),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.identifier", ".body.pull_request.id | tostring"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.title", ".body.pull_request.title"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.icon", "Terraform"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.team", "port"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.properties.author", ".body.pull_request.user.login"),
+ resource.TestCheckResourceAttr("port_webhook.create_pr", "mappings.0.entity.properties.url", ".body.pull_request.html_url"),
+ ),
+ },
+ {
+ ResourceName: "port_webhook.create_pr",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateId: webhookIdentifier,
+ },
+ },
+ })
+}
diff --git a/port/webhook/schema.go b/port/webhook/schema.go
new file mode 100644
index 00000000..79e4e974
--- /dev/null
+++ b/port/webhook/schema.go
@@ -0,0 +1,159 @@
+package webhook
+
+import (
+ "context"
+
+ "github.com/hashicorp/terraform-plugin-framework/resource"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+ "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"
+)
+
+func WebhookSecuritySchema() map[string]schema.Attribute {
+ return map[string]schema.Attribute{
+ "secret": schema.StringAttribute{
+ MarkdownDescription: "The secret of the webhook",
+ Optional: true,
+ },
+ "signature_header_name": schema.StringAttribute{
+ MarkdownDescription: "The signature header name of the webhook",
+ Optional: true,
+ },
+ "signature_algorithm": schema.StringAttribute{
+ MarkdownDescription: "The signature algorithm of the webhook",
+ Optional: true,
+ },
+ "signature_prefix": schema.StringAttribute{
+ MarkdownDescription: "The signature prefix of the webhook",
+ Optional: true,
+ },
+ "request_identifier_path": schema.StringAttribute{
+ MarkdownDescription: "The request identifier path of the webhook",
+ Optional: true,
+ },
+ }
+}
+
+func WebhookMappingSchema() map[string]schema.Attribute {
+ return map[string]schema.Attribute{
+ "blueprint": schema.StringAttribute{
+ MarkdownDescription: "The blueprint of the mapping",
+ Required: true,
+ },
+ "filter": schema.StringAttribute{
+ MarkdownDescription: "The filter of the mapping",
+ Optional: true,
+ },
+ "items_to_parse": schema.StringAttribute{
+ MarkdownDescription: "The items to parser of the mapping",
+ Optional: true,
+ },
+ "entity": schema.SingleNestedAttribute{
+ MarkdownDescription: "The entity of the mapping",
+ Required: true,
+ Attributes: map[string]schema.Attribute{
+ "identifier": schema.StringAttribute{
+ MarkdownDescription: "The identifier of the entity",
+ Required: true,
+ },
+ "title": schema.StringAttribute{
+ MarkdownDescription: "The title of the entity",
+ Optional: true,
+ },
+ "icon": schema.StringAttribute{
+ MarkdownDescription: "The icon of the entity",
+ Optional: true,
+ },
+ "team": schema.StringAttribute{
+ MarkdownDescription: "The team of the entity",
+ Optional: true,
+ },
+ "properties": schema.MapAttribute{
+ MarkdownDescription: "The properties of the entity",
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ "relations": schema.MapAttribute{
+ MarkdownDescription: "The relations of the entity",
+ Optional: true,
+ ElementType: types.StringType,
+ },
+ },
+ },
+ }
+}
+
+func WebhookSchema() map[string]schema.Attribute {
+ return map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "identifier": schema.StringAttribute{
+ MarkdownDescription: "The identifier of the webhook",
+ Optional: true,
+ Computed: true,
+ },
+ "title": schema.StringAttribute{
+ MarkdownDescription: "The title of the webhook",
+ Optional: true,
+ },
+ "icon": schema.StringAttribute{
+ MarkdownDescription: "The icon of the webhook",
+ Optional: true,
+ },
+ "description": schema.StringAttribute{
+ MarkdownDescription: "The description of the webhook",
+ Optional: true,
+ },
+ "enabled": schema.BoolAttribute{
+ MarkdownDescription: "Whether the webhook is enabled",
+ Optional: true,
+ Computed: true,
+ Default: booldefault.StaticBool(false),
+ },
+ "security": schema.SingleNestedAttribute{
+ MarkdownDescription: "The security of the webhook",
+ Optional: true,
+ Attributes: WebhookSecuritySchema(),
+ },
+
+ "mappings": schema.ListNestedAttribute{
+ MarkdownDescription: "The mappings of the webhook",
+ Optional: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: WebhookMappingSchema(),
+ },
+ },
+ "created_at": schema.StringAttribute{
+ MarkdownDescription: "The creation date of the webhook",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "created_by": schema.StringAttribute{
+ MarkdownDescription: "The creator of the webhook",
+ Computed: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ },
+ },
+ "updated_at": schema.StringAttribute{
+ MarkdownDescription: "The last update date of the webhook",
+ Computed: true,
+ },
+ "updated_by": schema.StringAttribute{
+ MarkdownDescription: "The last updater of the webhook",
+ Computed: true,
+ },
+ }
+}
+
+func (r *WebhookResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "Webhook resource",
+ Attributes: WebhookSchema(),
+ }
+}
diff --git a/port/webhook/webhookResourceToPortBody.go b/port/webhook/webhookResourceToPortBody.go
new file mode 100644
index 00000000..ab0f0e4e
--- /dev/null
+++ b/port/webhook/webhookResourceToPortBody.go
@@ -0,0 +1,114 @@
+package webhook
+
+import (
+ "context"
+
+ "github.com/port-labs/terraform-provider-port-labs/internal/cli"
+)
+
+func webhookResourceToPortBody(ctx context.Context, state *WebhookModel) (*cli.Webhook, error) {
+ w := &cli.Webhook{
+ Identifier: state.Identifier.ValueString(),
+ Security: &cli.Security{},
+ }
+
+ if !state.Icon.IsNull() {
+ icon := state.Icon.ValueString()
+ w.Icon = &icon
+ }
+
+ if !state.Title.IsNull() {
+ title := state.Title.ValueString()
+ w.Title = &title
+ }
+
+ if !state.Description.IsNull() {
+ description := state.Description.ValueString()
+ w.Description = &description
+ }
+
+ if !state.Enabled.IsNull() {
+ enabled := state.Enabled.ValueBool()
+ w.Enabled = &enabled
+ }
+ if state.Security != nil {
+ if !state.Security.Secret.IsNull() {
+ secret := state.Security.Secret.ValueString()
+ w.Security.Secret = &secret
+ }
+ if !state.Security.SignatureHeaderName.IsNull() {
+ signatureHeaderName := state.Security.SignatureHeaderName.ValueString()
+ w.Security.SignatureHeaderName = &signatureHeaderName
+ }
+ if !state.Security.SignatureAlgorithm.IsNull() {
+ signatureAlgorithm := state.Security.SignatureAlgorithm.ValueString()
+ w.Security.SignatureAlgorithm = &signatureAlgorithm
+ }
+ if !state.Security.SignaturePrefix.IsNull() {
+ signaturePrefix := state.Security.SignaturePrefix.ValueString()
+ w.Security.SignaturePrefix = &signaturePrefix
+ }
+
+ if !state.Security.RequestIdentifierPath.IsNull() {
+ requestIdentifierPath := state.Security.RequestIdentifierPath.ValueString()
+ w.Security.RequestIdentifierPath = &requestIdentifierPath
+ }
+ }
+
+ if len(state.Mappings) > 0 {
+ w.Mappings = []cli.Mappings{}
+ for _, v := range state.Mappings {
+ mapping := cli.Mappings{
+ Blueprint: v.Blueprint.ValueString(),
+ Entity: &cli.EntityProperty{
+ Identifier: v.Entity.Identifier.ValueString(),
+ },
+ }
+
+ if !v.Filter.IsNull() {
+ filter := v.Filter.ValueString()
+ mapping.Filter = &filter
+ }
+
+ if !v.ItemsToParse.IsNull() {
+ ItemsToParse := v.ItemsToParse.ValueString()
+ mapping.ItemsToParse = &ItemsToParse
+ }
+
+ if !v.Entity.Icon.IsNull() {
+ icon := v.Entity.Icon.ValueString()
+ mapping.Entity.Icon = &icon
+ }
+
+ if !v.Entity.Title.IsNull() {
+ title := v.Entity.Title.ValueString()
+ mapping.Entity.Title = &title
+ }
+
+ if !v.Entity.Team.IsNull() {
+ team := v.Entity.Team.ValueString()
+ mapping.Entity.Team = &team
+ }
+
+ if v.Entity.Properties != nil {
+ properties := make(map[string]string)
+ for k, v := range v.Entity.Properties {
+ properties[k] = v
+ }
+ mapping.Entity.Properties = properties
+ }
+
+ if v.Entity.Relations != nil {
+ relations := make(map[string]string)
+ for k, v := range v.Entity.Relations {
+ relations[k] = v
+ }
+ mapping.Entity.Relations = relations
+ }
+
+ w.Mappings = append(w.Mappings, mapping)
+ }
+ }
+
+ return w, nil
+}
diff --git a/provider/provider.go b/provider/provider.go
index 19328322..a007ea4b 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -13,6 +13,7 @@ import (
"github.com/port-labs/terraform-provider-port-labs/port/action"
"github.com/port-labs/terraform-provider-port-labs/port/blueprint"
"github.com/port-labs/terraform-provider-port-labs/port/entity"
+ "github.com/port-labs/terraform-provider-port-labs/port/webhook"
"github.com/port-labs/terraform-provider-port-labs/version"
)
@@ -124,6 +125,7 @@ func (p *PortLabsProvider) Resources(ctx context.Context) []func() resource.Reso
blueprint.NewBlueprintResource,
entity.NewEntityResource,
action.NewActionResource,
+ webhook.NewWebhookResource,
}
}