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, } }