Skip to content

Commit 9b55f10

Browse files
NickElliotc2thorn
authored andcommitted
make Firewall Policy Association mutable (GoogleCloudPlatform#13078)
Co-authored-by: Cameron Thornton <[email protected]>
1 parent 646502c commit 9b55f10

File tree

4 files changed

+181
-2
lines changed

4 files changed

+181
-2
lines changed

mmv1/products/compute/FirewallPolicyAssociation.yaml

+20-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ self_link: 'locations/global/firewallPolicies/{{firewall_policy}}/getAssociation
2828
create_url: 'locations/global/firewallPolicies/{{firewall_policy}}/addAssociation'
2929
delete_url: 'locations/global/firewallPolicies/{{firewall_policy}}/removeAssociation?name={{name}}'
3030
delete_verb: 'POST'
31-
immutable: true
31+
update_url: 'locations/global/firewallPolicies/{{firewall_policy}}/addAssociation?replaceExistingAssociation=true'
32+
update_verb: 'POST'
3233
import_format:
3334
- 'locations/global/firewallPolicies/{{firewall_policy}}/associations/{{name}}'
3435
- '{{firewall_policy}}/{{name}}'
@@ -37,6 +38,7 @@ timeouts:
3738
update_minutes: 20
3839
delete_minutes: 20
3940
custom_code:
41+
custom_update: 'templates/terraform/custom_update/firewall_policy_association_update.go.tmpl'
4042
pre_read: 'templates/terraform/pre_read/compute_firewall_policy_association.go.tmpl'
4143
post_create: 'templates/terraform/post_create/compute_firewall_policy_association_operation.go.tmpl'
4244
post_delete: 'templates/terraform/post_delete/compute_firewall_policy_association_operation.go.tmpl'
@@ -52,14 +54,28 @@ examples:
5254
test_env_vars:
5355
org_id: 'ORG_ID'
5456
exclude_test: true
57+
- name: 'firewall_policy_association_swapover'
58+
primary_resource_id: 'default'
59+
vars:
60+
policy_name: 'my-policy'
61+
association_name: 'my-association'
62+
folder_name: 'my-folder'
63+
test_env_vars:
64+
org_id: 'ORG_ID'
65+
exclude_test: true
5566
parameters:
5667
- name: 'firewallPolicy'
5768
type: ResourceRef
5869
description: |
5970
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.
6077
ignore_read: true
6178
required: true
62-
immutable: true
6379
diff_suppress_func: 'tpgresource.CompareResourceNames'
6480
custom_expand: 'templates/terraform/custom_expand/compute_firewall_policy_association.go.tmpl'
6581
resource: 'FirewallPolicy'
@@ -70,11 +86,13 @@ properties:
7086
description: |
7187
The name for an association.
7288
required: true
89+
immutable: true
7390
- name: 'attachmentTarget'
7491
type: String
7592
description: |
7693
The target that the firewall policy is attached to.
7794
required: true
95+
immutable: true
7896
diff_suppress_func: 'tpgresource.CompareSelfLinkOrResourceName'
7997
- name: 'shortName'
8098
type: String
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
2+
if err != nil {
3+
return err
4+
}
5+
6+
billingProject := ""
7+
8+
obj := make(map[string]interface{})
9+
nameProp, err := expandComputeFirewallPolicyAssociationName(d.Get("name"), d, config)
10+
if err != nil {
11+
return err
12+
} else if v, ok := d.GetOkExists("name"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, nameProp)) {
13+
obj["name"] = nameProp
14+
}
15+
attachmentTargetProp, err := expandComputeFirewallPolicyAssociationAttachmentTarget(d.Get("attachment_target"), d, config)
16+
if err != nil {
17+
return err
18+
} else if v, ok := d.GetOkExists("attachment_target"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, attachmentTargetProp)) {
19+
obj["attachmentTarget"] = attachmentTargetProp
20+
}
21+
firewallPolicyProp, err := expandComputeFirewallPolicyAssociationFirewallPolicy(d.Get("firewall_policy"), d, config)
22+
if err != nil {
23+
return err
24+
} else if v, ok := d.GetOkExists("firewall_policy"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, firewallPolicyProp)) {
25+
obj["firewallPolicy"] = firewallPolicyProp
26+
}
27+
28+
url, err := tpgresource.ReplaceVars(d, config, "{{"{{"}}ComputeBasePath{{"}}"}}locations/global/firewallPolicies/{{"{{"}}firewall_policy{{"}}"}}/addAssociation?replaceExistingAssociation=true")
29+
if err != nil {
30+
return err
31+
}
32+
33+
log.Printf("[DEBUG] Updating FirewallPolicyAssociation %q: %#v", d.Id(), obj)
34+
headers := make(http.Header)
35+
36+
// err == nil indicates that the billing_project value was found
37+
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
38+
billingProject = bp
39+
}
40+
41+
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
42+
Config: config,
43+
Method: "POST",
44+
Project: billingProject,
45+
RawURL: url,
46+
UserAgent: userAgent,
47+
Body: obj,
48+
Timeout: d.Timeout(schema.TimeoutUpdate),
49+
Headers: headers,
50+
})
51+
//following section is the area of the `custom_update` function for this resource that is different
52+
//`custom_update` was necessary over a `post_update"` because of the change to the error handler
53+
if err != nil {
54+
//before failing an update, restores the old firewall_policy value to prevent terraform state becoming broken
55+
parts := strings.Split(d.Id(), "/")
56+
oldPolicyPath := "locations/global/firewallPolicies/" + parts[len(parts)-3]
57+
d.Set("firewall_policy", oldPolicyPath)
58+
return fmt.Errorf("Error updating FirewallPolicyAssociation %q: %s", d.Id(), err)
59+
} else {
60+
log.Printf("[DEBUG] Finished updating FirewallPolicyAssociation %q: %#v", d.Id(), res)
61+
}
62+
63+
// store the ID now, needed because this update function is changing a normally immutable URL parameter field
64+
id, err := tpgresource.ReplaceVars(d, config, "locations/global/firewallPolicies/{{"{{"}}firewall_policy{{"}}"}}/associations/{{"{{"}}name{{"}}"}}")
65+
if err != nil {
66+
return fmt.Errorf("Error constructing id: %s", err)
67+
}
68+
d.SetId(id)
69+
70+
//following the swapover briefly both associations exist and this can cause operations to fail
71+
time.Sleep(60 * time.Second)
72+
//end `custom_update` changed zone
73+
74+
return resourceComputeFirewallPolicyAssociationRead(d, meta)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
resource "google_folder" "folder" {
2+
display_name = "{{index $.Vars "folder_name"}}"
3+
parent = "organizations/{{index $.TestEnvVars "org_id"}}"
4+
deletion_protection = false
5+
}
6+
7+
resource "google_compute_firewall_policy" "policy" {
8+
parent = "organizations/{{index $.TestEnvVars "org_id"}}"
9+
short_name = "{{index $.Vars "policy_name"}}" -> "{{index $.Vars "policy_name"}}-recreate"
10+
description = "Example Resource"
11+
lifecycle {
12+
create_before_destroy = true
13+
}
14+
}
15+
16+
resource "google_compute_firewall_policy_association" "{{$.PrimaryResourceId}}" {
17+
firewall_policy = google_compute_firewall_policy.policy.id
18+
attachment_target = google_folder.folder.name
19+
name = "{{index $.Vars "association_name"}}"
20+
}

mmv1/third_party/terraform/services/compute/resource_compute_firewall_policy_association_test.go

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

0 commit comments

Comments
 (0)