Skip to content

Commit 7b838de

Browse files
make Firewall Policy Association mutable (#13078) (#21735)
[upstream:061323912ec1d7b062d0e2534829476c20573975] Signed-off-by: Modular Magician <[email protected]>
1 parent e2dcae2 commit 7b838de

File tree

4 files changed

+188
-2
lines changed

4 files changed

+188
-2
lines changed

.changelog/13078.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
compute: update support has been added for the `firewall_policy` field in `google_compute_firewall_policy_association` resource. It is recommended to only perform this operation in combination with a protective lifecycle tag such as "create_before_destroy" or "prevent_destroy" on your previous `firewall_policy` resource in order to prevent situations where a target attachment has no associated policy.
3+
```

google/services/compute/resource_compute_firewall_policy_association.go

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"log"
2525
"net/http"
2626
"reflect"
27+
"strings"
2728
"time"
2829

2930
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
@@ -37,6 +38,7 @@ func ResourceComputeFirewallPolicyAssociation() *schema.Resource {
3738
return &schema.Resource{
3839
Create: resourceComputeFirewallPolicyAssociationCreate,
3940
Read: resourceComputeFirewallPolicyAssociationRead,
41+
Update: resourceComputeFirewallPolicyAssociationUpdate,
4042
Delete: resourceComputeFirewallPolicyAssociationDelete,
4143

4244
Importer: &schema.ResourceImporter{
@@ -45,6 +47,7 @@ func ResourceComputeFirewallPolicyAssociation() *schema.Resource {
4547

4648
Timeouts: &schema.ResourceTimeout{
4749
Create: schema.DefaultTimeout(20 * time.Minute),
50+
Update: schema.DefaultTimeout(20 * time.Minute),
4851
Delete: schema.DefaultTimeout(20 * time.Minute),
4952
},
5053

@@ -63,9 +66,14 @@ func ResourceComputeFirewallPolicyAssociation() *schema.Resource {
6366
"firewall_policy": {
6467
Type: schema.TypeString,
6568
Required: true,
66-
ForceNew: true,
6769
DiffSuppressFunc: tpgresource.CompareResourceNames,
68-
Description: `The firewall policy of the resource.`,
70+
Description: `The firewall policy of the resource.
71+
72+
This field can be updated to refer to a different Firewall Policy, which will create a new association from that new
73+
firewall policy with the flag to override the existing attachmentTarget's policy association.
74+
75+
**Note** Due to potential risks with this operation it is *highly* recommended to use the 'create_before_destroy' life cycle option
76+
on your exisiting firewall policy so as to prevent a situation where your attachment target has no associated policy.`,
6977
},
7078
"name": {
7179
Type: schema.TypeString,
@@ -212,6 +220,84 @@ func resourceComputeFirewallPolicyAssociationRead(d *schema.ResourceData, meta i
212220
return nil
213221
}
214222

223+
func resourceComputeFirewallPolicyAssociationUpdate(d *schema.ResourceData, meta interface{}) error {
224+
config := meta.(*transport_tpg.Config)
225+
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
226+
if err != nil {
227+
return err
228+
}
229+
230+
billingProject := ""
231+
232+
obj := make(map[string]interface{})
233+
nameProp, err := expandComputeFirewallPolicyAssociationName(d.Get("name"), d, config)
234+
if err != nil {
235+
return err
236+
} else if v, ok := d.GetOkExists("name"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, nameProp)) {
237+
obj["name"] = nameProp
238+
}
239+
attachmentTargetProp, err := expandComputeFirewallPolicyAssociationAttachmentTarget(d.Get("attachment_target"), d, config)
240+
if err != nil {
241+
return err
242+
} else if v, ok := d.GetOkExists("attachment_target"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, attachmentTargetProp)) {
243+
obj["attachmentTarget"] = attachmentTargetProp
244+
}
245+
firewallPolicyProp, err := expandComputeFirewallPolicyAssociationFirewallPolicy(d.Get("firewall_policy"), d, config)
246+
if err != nil {
247+
return err
248+
} else if v, ok := d.GetOkExists("firewall_policy"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, firewallPolicyProp)) {
249+
obj["firewallPolicy"] = firewallPolicyProp
250+
}
251+
252+
url, err := tpgresource.ReplaceVars(d, config, "{{ComputeBasePath}}locations/global/firewallPolicies/{{firewall_policy}}/addAssociation?replaceExistingAssociation=true")
253+
if err != nil {
254+
return err
255+
}
256+
257+
log.Printf("[DEBUG] Updating FirewallPolicyAssociation %q: %#v", d.Id(), obj)
258+
headers := make(http.Header)
259+
260+
// err == nil indicates that the billing_project value was found
261+
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
262+
billingProject = bp
263+
}
264+
265+
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
266+
Config: config,
267+
Method: "POST",
268+
Project: billingProject,
269+
RawURL: url,
270+
UserAgent: userAgent,
271+
Body: obj,
272+
Timeout: d.Timeout(schema.TimeoutUpdate),
273+
Headers: headers,
274+
})
275+
//following section is the area of the `custom_update` function for this resource that is different
276+
//`custom_update` was necessary over a `post_update"` because of the change to the error handler
277+
if err != nil {
278+
//before failing an update, restores the old firewall_policy value to prevent terraform state becoming broken
279+
parts := strings.Split(d.Id(), "/")
280+
oldPolicyPath := "locations/global/firewallPolicies/" + parts[len(parts)-3]
281+
d.Set("firewall_policy", oldPolicyPath)
282+
return fmt.Errorf("Error updating FirewallPolicyAssociation %q: %s", d.Id(), err)
283+
} else {
284+
log.Printf("[DEBUG] Finished updating FirewallPolicyAssociation %q: %#v", d.Id(), res)
285+
}
286+
287+
// store the ID now, needed because this update function is changing a normally immutable URL parameter field
288+
id, err := tpgresource.ReplaceVars(d, config, "locations/global/firewallPolicies/{{firewall_policy}}/associations/{{name}}")
289+
if err != nil {
290+
return fmt.Errorf("Error constructing id: %s", err)
291+
}
292+
d.SetId(id)
293+
294+
//following the swapover briefly both associations exist and this can cause operations to fail
295+
time.Sleep(60 * time.Second)
296+
//end `custom_update` changed zone
297+
298+
return resourceComputeFirewallPolicyAssociationRead(d, meta)
299+
}
300+
215301
func resourceComputeFirewallPolicyAssociationDelete(d *schema.ResourceData, meta interface{}) error {
216302
config := meta.(*transport_tpg.Config)
217303
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)

google/services/compute/resource_compute_firewall_policy_association_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,70 @@ resource "google_compute_firewall_policy_association" "default" {
114114
}
115115
`, context)
116116
}
117+
118+
func TestAccComputeFirewallPolicyAssociation_swapover(t *testing.T) {
119+
t.Parallel()
120+
121+
context := map[string]interface{}{
122+
"random_suffix": acctest.RandString(t, 10),
123+
"org_name": fmt.Sprintf("organizations/%s", envvar.GetTestOrgFromEnv(t)),
124+
}
125+
126+
acctest.VcrTest(t, resource.TestCase{
127+
PreCheck: func() { acctest.AccTestPreCheck(t) },
128+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
129+
Steps: []resource.TestStep{
130+
{
131+
Config: testAccComputeFirewallPolicyAssociation_basic(context),
132+
},
133+
{
134+
ResourceName: "google_compute_firewall_policy_association.default",
135+
ImportState: true,
136+
ImportStateVerify: true,
137+
// Referencing using ID causes import to fail
138+
ImportStateVerifyIgnore: []string{"firewall_policy"},
139+
},
140+
{
141+
Config: testAccComputeFirewallPolicyAssociation_swapover(context),
142+
},
143+
{
144+
ResourceName: "google_compute_firewall_policy_association.default",
145+
ImportState: true,
146+
ImportStateVerify: true,
147+
// Referencing using ID causes import to fail
148+
ImportStateVerifyIgnore: []string{"firewall_policy"},
149+
},
150+
},
151+
})
152+
}
153+
154+
func testAccComputeFirewallPolicyAssociation_swapover(context map[string]interface{}) string {
155+
return acctest.Nprintf(`
156+
resource "google_folder" "folder" {
157+
display_name = "tf-test-folder-%{random_suffix}"
158+
parent = "%{org_name}"
159+
deletion_protection = false
160+
}
161+
162+
resource "google_folder" "target_folder" {
163+
display_name = "tf-test-target-%{random_suffix}"
164+
parent = "%{org_name}"
165+
deletion_protection = false
166+
}
167+
168+
resource "google_compute_firewall_policy" "default" {
169+
parent = google_folder.folder.id
170+
short_name = "tf-test-policy-%{random_suffix}-swapover"
171+
description = "Resource created for Terraform acceptance testing"
172+
lifecycle {
173+
create_before_destroy = true
174+
}
175+
}
176+
177+
resource "google_compute_firewall_policy_association" "default" {
178+
firewall_policy = google_compute_firewall_policy.default.id
179+
attachment_target = google_folder.target_folder.name
180+
name = "tf-test-association-%{random_suffix}"
181+
}
182+
`, context)
183+
}

website/docs/r/compute_firewall_policy_association.html.markdown

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,31 @@ resource "google_compute_firewall_policy" "policy" {
4545
description = "Example Resource"
4646
}
4747
48+
resource "google_compute_firewall_policy_association" "default" {
49+
firewall_policy = google_compute_firewall_policy.policy.id
50+
attachment_target = google_folder.folder.name
51+
name = "my-association"
52+
}
53+
```
54+
## Example Usage - Firewall Policy Association Swapover
55+
56+
57+
```hcl
58+
resource "google_folder" "folder" {
59+
display_name = "my-folder"
60+
parent = "organizations/123456789"
61+
deletion_protection = false
62+
}
63+
64+
resource "google_compute_firewall_policy" "policy" {
65+
parent = "organizations/123456789"
66+
short_name = "my-policy" -> "my-policy-recreate"
67+
description = "Example Resource"
68+
lifecycle {
69+
create_before_destroy = true
70+
}
71+
}
72+
4873
resource "google_compute_firewall_policy_association" "default" {
4974
firewall_policy = google_compute_firewall_policy.policy.id
5075
attachment_target = google_folder.folder.name
@@ -68,6 +93,10 @@ The following arguments are supported:
6893
* `firewall_policy` -
6994
(Required)
7095
The firewall policy of the resource.
96+
This field can be updated to refer to a different Firewall Policy, which will create a new association from that new
97+
firewall policy with the flag to override the existing attachmentTarget's policy association.
98+
**Note** Due to potential risks with this operation it is *highly* recommended to use the `create_before_destroy` life cycle option
99+
on your exisiting firewall policy so as to prevent a situation where your attachment target has no associated policy.
71100

72101

73102
- - -
@@ -90,6 +119,7 @@ This resource provides the following
90119
[Timeouts](https://developer.hashicorp.com/terraform/plugin/sdkv2/resources/retries-and-customizable-timeouts) configuration options:
91120

92121
- `create` - Default is 20 minutes.
122+
- `update` - Default is 20 minutes.
93123
- `delete` - Default is 20 minutes.
94124

95125
## Import

0 commit comments

Comments
 (0)