Skip to content

Commit cccd867

Browse files
authored
Merge pull request #276 from port-labs/PORT-16077-bug-port-webhook-resource-does-not-support-object-values-for-identifier-field
Enhance port_webhook resource to support complex identifiers
2 parents 72ac902 + bec386a commit cccd867

File tree

7 files changed

+619
-10
lines changed

7 files changed

+619
-10
lines changed

docs/resources/port_webhook.md

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,46 @@ description: |-
9898
port_blueprint.team
9999
]
100100
101+
}
102+
# Example with complex identifier using search query
103+
resource "portwebhook" "complexidentifier" {
104+
identifier = "complexidentifierwebhook"
105+
title = "Webhook with complex identifier"
106+
icon = "Terraform"
107+
enabled = true
108+
mappings = [
109+
{
110+
blueprint = port_blueprint.microservice.identifier
111+
operation = { "type" = "create" }
112+
filter = ".headers.\"x-github-event\" == \"push\""
113+
entity = {
114+
# Complex identifier using search query to find entity by ARN
115+
identifier = jsonencode({
116+
combinator = "'and'",
117+
rules = [
118+
{
119+
property = "'arn'"
120+
operator = "'='"
121+
value = ".body.resources[0]"
122+
}
123+
]
124+
})
125+
title = ".body.repository.name"
126+
properties = {
127+
url = ".body.repository.html_url"
128+
}
129+
}
130+
}
131+
]
132+
133+
depends_on = [
134+
port_blueprint.microservice
135+
]
136+
101137
}
102138
```
103139
Notes
104-
When using object format for relations, combinator, property and operator fields should be enclosed in single quotes, while value should not have quotes as it's a JQ expression. The single quotes are required because these fields contain literal string values that must be passed as-is to the Port API, whereas value contains a JQ expression that should be evaluated dynamically.For all available operators, see the Port comparison operators documentation https://docs.port.io/search-and-query/comparison-operators.
140+
When using object format for relations, combinator, property and operator fields should be enclosed in single quotes, while value should not have quotes as it's a JQ expression. The single quotes are required because these fields contain literal string values that must be passed as-is to the Port API, whereas value contains a JQ expression that should be evaluated dynamically.The identifier field supports both simple JQ expressions (strings) and complex search query objects. When using search query objects, the structure must include combinator and rules fields, and each rule must have property, operator, and value fields.For all available operators, see the Port comparison operators documentation https://docs.port.io/search-and-query/comparison-operators.
105141
---
106142

107143
# port_webhook (Resource)
@@ -208,11 +244,49 @@ resource "port_blueprint" "author" {
208244
]
209245
}
210246
247+
# Example with complex identifier using search query
248+
resource "port_webhook" "complex_identifier" {
249+
identifier = "complex_identifier_webhook"
250+
title = "Webhook with complex identifier"
251+
icon = "Terraform"
252+
enabled = true
253+
254+
mappings = [
255+
{
256+
blueprint = port_blueprint.microservice.identifier
257+
operation = { "type" = "create" }
258+
filter = ".headers.\"x-github-event\" == \"push\""
259+
entity = {
260+
# Complex identifier using search query to find entity by ARN
261+
identifier = jsonencode({
262+
combinator = "'and'",
263+
rules = [
264+
{
265+
property = "'arn'"
266+
operator = "'='"
267+
value = ".body.resources[0]"
268+
}
269+
]
270+
})
271+
title = ".body.repository.name"
272+
properties = {
273+
url = ".body.repository.html_url"
274+
}
275+
}
276+
}
277+
]
278+
279+
depends_on = [
280+
port_blueprint.microservice
281+
]
282+
}
283+
211284
```
212285

213286
## Notes
214287

215288
- When using object format for relations, `combinator`, `property` and `operator` fields should be enclosed in single quotes, while `value` should not have quotes as it's a JQ expression. The single quotes are required because these fields contain literal string values that must be passed as-is to the Port API, whereas `value` contains a JQ expression that should be evaluated dynamically.
289+
- The `identifier` field supports both simple JQ expressions (strings) and complex search query objects. When using search query objects, the structure must include `combinator` and `rules` fields, and each rule must have `property`, `operator`, and `value` fields.
216290
- For all available operators, see the [Port comparison operators documentation](https://docs.port.io/search-and-query/comparison-operators).
217291

218292

@@ -259,7 +333,7 @@ Optional:
259333

260334
Required:
261335

262-
- `identifier` (String) The identifier of the entity
336+
- `identifier` (String) The identifier of the entity. Can be either a simple JQ expression (string) or a search query object encoded with jsonencode(). When using search query objects, the structure must include 'combinator' and 'rules' fields, and each rule must have 'property', 'operator', and 'value' fields.
263337

264338
Optional:
265339

internal/cli/models.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ type (
414414
}
415415

416416
EntityProperty struct {
417-
Identifier string `json:"identifier,omitempty"`
417+
Identifier any `json:"identifier,omitempty"`
418418
Title *string `json:"title,omitempty"`
419419
Icon *string `json:"icon,omitempty"`
420420
Team *string `json:"team,omitempty"`

main.tf

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
terraform {
2+
required_providers {
3+
port = {
4+
source = "port-labs/port-labs"
5+
version = "0.9.6"
6+
}
7+
}
8+
}
9+
10+
provider "port" {
11+
client_id = "60EsooJtOqimlekxrNh7nfr2iOgTcyLZ" # or set the env var PORT_CLIENT_ID
12+
secret = "1yp6DQM6OwW4svTEIbrEFthMQqVVrIQ3fcJK0aJUpMwdc07ZuA6v27C3G08oj8Kx" # or set the env var PORT_CLIENT_SECRET
13+
base_url = "http://api.localhost:9080" # or set the env var PORT_BASE_URL
14+
}
15+
16+
resource "port_blueprint" "author" {
17+
title = "Author"
18+
icon = "User"
19+
identifier = "author"
20+
properties = {
21+
string_props = {
22+
"name" = {
23+
type = "string"
24+
title = "Name"
25+
}
26+
}
27+
}
28+
}
29+
30+
resource "port_blueprint" "team" {
31+
title = "Team"
32+
icon = "Team"
33+
identifier = "team"
34+
properties = {
35+
string_props = {
36+
"name" = {
37+
type = "string"
38+
title = "Team Name"
39+
}
40+
}
41+
}
42+
}
43+
44+
resource "port_blueprint" "microservice" {
45+
title = "TF test microservice"
46+
icon = "Terraform"
47+
identifier = "microservice"
48+
properties = {
49+
string_props = {
50+
"url" = {
51+
type = "string"
52+
title = "URL"
53+
}
54+
}
55+
}
56+
relations = {
57+
"author" = {
58+
title = "Author"
59+
target = port_blueprint.author.identifier
60+
}
61+
"team" = {
62+
title = "Team"
63+
target = port_blueprint.team.identifier
64+
}
65+
}
66+
}
67+
68+
69+
# resource "port_webhook" "create_pr" {
70+
# identifier = "pr_webhook"
71+
# title = "Webhook with mixed relations"
72+
# icon = "Terraform"
73+
# enabled = true
74+
75+
# mappings = [
76+
# {
77+
# blueprint = port_blueprint.microservice.identifier
78+
# operation = { "type" = "create" }
79+
# filter = ".headers.\"x-github-event\" == \"pull_request\""
80+
# entity = {
81+
# identifier = ".body.pull_request.id | tostring"
82+
# title = ".body.pull_request.title"
83+
# properties = {
84+
# url = ".body.pull_request.html_url"
85+
# }
86+
# relations = {
87+
# # Complex object relation with search query
88+
# author = jsonencode({
89+
# combinator = "'and'",
90+
# rules = [
91+
# {
92+
# property = "'$identifier'"
93+
# operator = "'='"
94+
# value = ".body.pull_request.user.login | tostring"
95+
# }
96+
# ]
97+
# })
98+
99+
# # Simple string relation
100+
# team = ".body.repository.owner.login | tostring"
101+
# }
102+
# }
103+
# }
104+
# ]
105+
106+
# depends_on = [
107+
# port_blueprint.microservice,
108+
# port_blueprint.author,
109+
# port_blueprint.team
110+
# ]
111+
# }
112+
113+
# resource "port_webhook" "test_webhook_1" {
114+
# identifier = "testWebhook1"
115+
# title = "Webhook with string relation"
116+
# enabled = true
117+
# mappings = [
118+
# {
119+
# blueprint = "githubPullRequest"
120+
# operation = { "type" = "create" }
121+
# filter = ".headers.\"x-github-event\" == \"pull_request\""
122+
# entity = {
123+
# identifier = ".body.pull_request.id | tostring"
124+
# title = ".body.pull_request.title"
125+
# url = ".body.pull_request.html_url"
126+
# relations = {
127+
# author = jsonencode({
128+
# rules = [
129+
# {
130+
# operator = "'='"
131+
# property = "'$identifier'"
132+
# value = ".body.pull_request.user.login | tostring"
133+
# }
134+
# ]
135+
# })
136+
# }
137+
# }
138+
# }
139+
# ]
140+
# }
141+
142+
# resource "port_webhook" "test_webhook_2" {
143+
# identifier = "testWebhook2"
144+
# title = "Webhook with json relation"
145+
# enabled = true
146+
# mappings = [
147+
# {
148+
# blueprint = "githubPullRequest"
149+
# operation = { "type" = "create" }
150+
# filter = ".headers.\"x-github-event\" == \"pull_request\""
151+
# entity = {
152+
# identifier = ".body.pull_request.id | tostring"
153+
# title = ".body.pull_request.title"
154+
# url = ".body.pull_request.html_url"
155+
# relations = {
156+
# author = jsonencode({
157+
# combinator = "'and'",
158+
# rules = [
159+
# {
160+
# operator = "'='"
161+
# property = "'$identifier'"
162+
# value = ".body.pull_request.user.login | tostring"
163+
# }
164+
# ]
165+
# })
166+
# }
167+
# }
168+
# }
169+
# ]
170+
# }
171+
172+
resource "port_webhook" "test_webhook_3" {
173+
identifier = "testWebhook32"
174+
title = "wh with both relations"
175+
enabled = true
176+
mappings = [
177+
{
178+
blueprint = "githubPullRequest"
179+
operation = { "type" = "create" }
180+
filter = ".headers.\"x-github-event\" == \"pull_request\""
181+
entity = {
182+
identifier = ".body.pull_request.id | tostring"
183+
title = ".body.pull_request.title"
184+
url = ".body.pull_request.html_url"
185+
# relations = {
186+
# author = jsonencode({
187+
# combinator = "'and'",
188+
# rules = [
189+
# {
190+
# operator = "'='"
191+
# property = "'$identifier'"
192+
# value = ".body.pull_request.user.login | tostring"
193+
# }
194+
# ]
195+
# })
196+
# team = ".body.repository.owner.login | tostring"
197+
# }
198+
}
199+
}
200+
]
201+
}
202+
203+
resource "port_webhook" "test_webhook_4" {
204+
identifier = "testWebhook3"
205+
title = "wh with both relations"
206+
enabled = true
207+
mappings = [
208+
{
209+
blueprint = "githubPullRequest"
210+
operation = { "type" = "create" }
211+
filter = ".headers.\"x-github-event\" == \"pull_request\""
212+
entity = {
213+
# identifier = ".body.pull_request.id | tostring"
214+
identifier = jsonencode({
215+
combinator = "'and'",
216+
rules = [
217+
{
218+
property = "'$identifier'"
219+
operator = "'='"
220+
value = ".body.pull_request.user.login | tostring"
221+
}
222+
]
223+
})
224+
title = ".body.pull_request.title"
225+
url = ".body.pull_request.html_url"
226+
# relations = {
227+
# author = jsonencode({
228+
# combinator = "'and'",
229+
# rules = [
230+
# {
231+
# operator = "'='"
232+
# property = "'$identifier'"
233+
# value = ".body.pull_request.user.login | tostring"
234+
# }
235+
# ]
236+
# })
237+
# team = ".body.repository.owner.login | tostring"
238+
# }
239+
}
240+
}
241+
]
242+
}

port/webhook/refreshWebhookState.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,20 @@ func refreshWebhookState(ctx context.Context, state *WebhookModel, w *cli.Webhoo
3939
for _, v := range w.Mappings {
4040
mapping := &MappingsModel{
4141
Blueprint: types.StringValue(v.Blueprint),
42-
Entity: &EntityModel{
43-
Identifier: types.StringValue(v.Entity.Identifier),
44-
},
42+
Entity: &EntityModel{},
43+
}
44+
45+
switch identifier := v.Entity.Identifier.(type) {
46+
case string:
47+
mapping.Entity.Identifier = types.StringValue(identifier)
48+
case map[string]interface{}:
49+
if jsonBytes, err := json.Marshal(identifier); err == nil {
50+
mapping.Entity.Identifier = types.StringValue(string(jsonBytes))
51+
} else {
52+
return fmt.Errorf("failed to marshal identifier to JSON: %w", err)
53+
}
54+
default:
55+
return fmt.Errorf("invalid identifier type: expected string or object, got %T", identifier)
4556
}
4657

4758
mapping.Filter = flex.GoStringToFramework(v.Filter)

0 commit comments

Comments
 (0)