Skip to content

Commit c4d286b

Browse files
Compute - Add update support for Network IP when changing network/subnetwork (#4030) (#2590)
* Compute - Add update support for Network IP when changing network/subnetwork * add field required by beta provider * error check for d.ForceNew * refactor functions into own file. introduce network-interface-helper * refresh instance after deleting alias config * correct bad merge * spelling fix * check error * Add forcenew unit test * resolve build issue * Update third_party/terraform/tests/resource_compute_instance_test.go.erb Co-authored-by: Cameron Thornton <[email protected]> * Update third_party/terraform/tests/resource_compute_instance_test.go.erb Co-authored-by: Cameron Thornton <[email protected]> * error couldn't convert to string * to type string * Refactored some test code for cleaner style and condensed some if statements * resolve merge issue with megan's change * update network interface helper to promote better go readability * fixed instances where incorrect name was referenced * removed extraneous dependencies and extrapolated self mutating operations * scrap refactor. only extrapolate functions when needed. * error check * resolved some fomatting and comment concerns. * find a comment a nice cozy new home on the hill side amongst all its comment friends. Truly truly a beautiful site to behold. Please be well and safe comment because the world needs you. YOU TOO are important * a comma boi * another commma for the party Co-authored-by: Cameron Thornton <[email protected]> Signed-off-by: Modular Magician <[email protected]> Co-authored-by: Cameron Thornton <[email protected]>
1 parent b13ea50 commit c4d286b

File tree

6 files changed

+308
-54
lines changed

6 files changed

+308
-54
lines changed

.changelog/4030.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
```release-note:enhancement
2+
compute: added support for updating `network_interface.[d].network_ip` on `google_compute_instance` when changing network or subnetwork
3+
4+
```
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package google
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/errwrap"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8+
computeBeta "google.golang.org/api/compute/v0.beta"
9+
)
10+
11+
func computeInstanceDeleteAccessConfigs(d *schema.ResourceData, config *Config, instNetworkInterface *computeBeta.NetworkInterface, project, zone, userAgent, instanceName string) error {
12+
// Delete any accessConfig that currently exists in instNetworkInterface
13+
for _, ac := range instNetworkInterface.AccessConfigs {
14+
op, err := config.NewComputeClient(userAgent).Instances.DeleteAccessConfig(
15+
project, zone, instanceName, ac.Name, instNetworkInterface.Name).Do()
16+
if err != nil {
17+
return fmt.Errorf("Error deleting old access_config: %s", err)
18+
}
19+
opErr := computeOperationWaitTime(config, op, project, "old access_config to delete", userAgent, d.Timeout(schema.TimeoutUpdate))
20+
if opErr != nil {
21+
return opErr
22+
}
23+
}
24+
return nil
25+
}
26+
27+
func computeInstanceAddAccessConfigs(d *schema.ResourceData, config *Config, instNetworkInterface *computeBeta.NetworkInterface, accessConfigs []*computeBeta.AccessConfig, project, zone, userAgent, instanceName string) error {
28+
// Create new ones
29+
for _, ac := range accessConfigs {
30+
op, err := config.NewComputeBetaClient(userAgent).Instances.AddAccessConfig(project, zone, instanceName, instNetworkInterface.Name, ac).Do()
31+
if err != nil {
32+
return fmt.Errorf("Error adding new access_config: %s", err)
33+
}
34+
opErr := computeOperationWaitTime(config, op, project, "new access_config to add", userAgent, d.Timeout(schema.TimeoutUpdate))
35+
if opErr != nil {
36+
return opErr
37+
}
38+
}
39+
return nil
40+
}
41+
42+
func computeInstanceCreateUpdateWhileStoppedCall(d *schema.ResourceData, config *Config, networkInterfacePatchObj *computeBeta.NetworkInterface, accessConfigs []*computeBeta.AccessConfig, accessConfigsHaveChanged bool, index int, project, zone, userAgent, instanceName string) func(inst *computeBeta.Instance) error {
43+
44+
// Access configs' ip changes when the instance stops invalidating our fingerprint
45+
// expect caller to re-validate instance before calling patch this is why we expect
46+
// instance to be passed in
47+
return func(instance *computeBeta.Instance) error {
48+
49+
instNetworkInterface := instance.NetworkInterfaces[index]
50+
networkInterfacePatchObj.Fingerprint = instNetworkInterface.Fingerprint
51+
52+
// Access config can run into some issues since we can't tell the difference between
53+
// the users declared intent (config within their hcl file) and what we have inferred from the
54+
// server (terraform state). Access configs contain an ip subproperty that can be incompatible
55+
// with the subnetwork/network we are transitioning to. Due to this we only change access
56+
// configs if we notice the configuration (user intent) changes.
57+
if accessConfigsHaveChanged {
58+
err := computeInstanceDeleteAccessConfigs(d, config, instNetworkInterface, project, zone, userAgent, instanceName)
59+
if err != nil {
60+
return err
61+
}
62+
}
63+
64+
op, err := config.NewComputeBetaClient(userAgent).Instances.UpdateNetworkInterface(project, zone, instanceName, instNetworkInterface.Name, networkInterfacePatchObj).Do()
65+
if err != nil {
66+
return errwrap.Wrapf("Error updating network interface: {{err}}", err)
67+
}
68+
opErr := computeOperationWaitTime(config, op, project, "network interface to update", userAgent, d.Timeout(schema.TimeoutUpdate))
69+
if opErr != nil {
70+
return opErr
71+
}
72+
73+
if accessConfigsHaveChanged {
74+
err := computeInstanceAddAccessConfigs(d, config, instNetworkInterface, accessConfigs, project, zone, userAgent, instanceName)
75+
if err != nil {
76+
return err
77+
}
78+
}
79+
return nil
80+
}
81+
}

google-beta/resource_compute_instance.go

Lines changed: 81 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import (
1919
"github.com/mitchellh/hashstructure"
2020
computeBeta "google.golang.org/api/compute/v0.beta"
2121
"google.golang.org/api/compute/v1"
22-
"google.golang.org/api/googleapi"
2322
)
2423

2524
var (
@@ -55,6 +54,38 @@ var (
5554
}
5655
)
5756

57+
// network_interface.[d].network_ip can only change when subnet/network
58+
// is also changing. Validate that if network_ip is changing this scenario
59+
// holds up to par.
60+
func forceNewIfNetworkIPNotUpdatable(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error {
61+
// separate func to allow unit testing
62+
return forceNewIfNetworkIPNotUpdatableFunc(d)
63+
}
64+
65+
func forceNewIfNetworkIPNotUpdatableFunc(d TerraformResourceDiff) error {
66+
oldCount, newCount := d.GetChange("network_interface.#")
67+
if oldCount.(int) != newCount.(int) {
68+
return nil
69+
}
70+
71+
for i := 0; i < newCount.(int); i++ {
72+
prefix := fmt.Sprintf("network_interface.%d", i)
73+
networkKey := prefix + ".network"
74+
subnetworkKey := prefix + ".subnetwork"
75+
subnetworkProjectKey := prefix + ".subnetwork_project"
76+
networkIPKey := prefix + ".network_ip"
77+
if d.HasChange(networkIPKey) {
78+
if !d.HasChange(networkKey) && !d.HasChange(subnetworkKey) && !d.HasChange(subnetworkProjectKey) {
79+
if err := d.ForceNew(networkIPKey); err != nil {
80+
return err
81+
}
82+
}
83+
}
84+
}
85+
86+
return nil
87+
}
88+
5889
func resourceComputeInstance() *schema.Resource {
5990
return &schema.Resource{
6091
Create: resourceComputeInstanceCreate,
@@ -253,7 +284,6 @@ func resourceComputeInstance() *schema.Resource {
253284
"network_ip": {
254285
Type: schema.TypeString,
255286
Optional: true,
256-
ForceNew: true,
257287
Computed: true,
258288
Description: `The private IP address assigned to the instance.`,
259289
},
@@ -710,6 +740,7 @@ func resourceComputeInstance() *schema.Resource {
710740
suppressEmptyGuestAcceleratorDiff,
711741
),
712742
desiredStatusDiff,
743+
forceNewIfNetworkIPNotUpdatable,
713744
),
714745
}
715746
}
@@ -1336,7 +1367,7 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
13361367
return fmt.Errorf("Instance had unexpected number of network interfaces: %d", len(instance.NetworkInterfaces))
13371368
}
13381369

1339-
var updatesToNIWhileStopped []func(...googleapi.CallOption) (*computeBeta.Operation, error)
1370+
var updatesToNIWhileStopped []func(inst *computeBeta.Instance) error
13401371
for i := 0; i < len(networkInterfaces); i++ {
13411372
prefix := fmt.Sprintf("network_interface.%d", i)
13421373
networkInterface := networkInterfaces[i]
@@ -1377,50 +1408,23 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
13771408
}
13781409
}
13791410

1380-
if d.HasChange(prefix + ".access_config") {
1381-
1411+
if !updateDuringStop && d.HasChange(prefix+".access_config") {
13821412
// TODO: This code deletes then recreates accessConfigs. This is bad because it may
13831413
// leave the machine inaccessible from either ip if the creation part fails (network
13841414
// timeout etc). However right now there is a GCE limit of 1 accessConfig so it is
13851415
// the only way to do it. In future this should be revised to only change what is
13861416
// necessary, and also add before removing.
13871417

1388-
// Delete any accessConfig that currently exists in instNetworkInterface
1389-
for _, ac := range instNetworkInterface.AccessConfigs {
1390-
op, err := config.NewComputeClient(userAgent).Instances.DeleteAccessConfig(
1391-
project, zone, instance.Name, ac.Name, networkName).Do()
1392-
if err != nil {
1393-
return fmt.Errorf("Error deleting old access_config: %s", err)
1394-
}
1395-
opErr := computeOperationWaitTime(config, op, project, "old access_config to delete", userAgent, d.Timeout(schema.TimeoutUpdate))
1396-
if opErr != nil {
1397-
return opErr
1398-
}
1418+
// Delete current access configs
1419+
err := computeInstanceDeleteAccessConfigs(d, config, instNetworkInterface, project, zone, userAgent, instance.Name)
1420+
if err != nil {
1421+
return err
13991422
}
14001423

14011424
// Create new ones
1402-
accessConfigsCount := d.Get(prefix + ".access_config.#").(int)
1403-
for j := 0; j < accessConfigsCount; j++ {
1404-
acPrefix := fmt.Sprintf("%s.access_config.%d", prefix, j)
1405-
ac := &computeBeta.AccessConfig{
1406-
Type: "ONE_TO_ONE_NAT",
1407-
NatIP: d.Get(acPrefix + ".nat_ip").(string),
1408-
NetworkTier: d.Get(acPrefix + ".network_tier").(string),
1409-
}
1410-
if ptr, ok := d.GetOk(acPrefix + ".public_ptr_domain_name"); ok && ptr != "" {
1411-
ac.SetPublicPtr = true
1412-
ac.PublicPtrDomainName = ptr.(string)
1413-
}
1414-
1415-
op, err := config.NewComputeBetaClient(userAgent).Instances.AddAccessConfig(
1416-
project, zone, instance.Name, networkName, ac).Do()
1417-
if err != nil {
1418-
return fmt.Errorf("Error adding new access_config: %s", err)
1419-
}
1420-
opErr := computeOperationWaitTime(config, op, project, "new access_config to add", userAgent, d.Timeout(schema.TimeoutUpdate))
1421-
if opErr != nil {
1422-
return opErr
1423-
}
1425+
err = computeInstanceAddAccessConfigs(d, config, instNetworkInterface, networkInterface.AccessConfigs, project, zone, userAgent, instance.Name)
1426+
if err != nil {
1427+
return err
14241428
}
14251429

14261430
// re-read fingerprint
@@ -1431,10 +1435,6 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
14311435
instNetworkInterface = instance.NetworkInterfaces[i]
14321436
}
14331437

1434-
// Setting NetworkIP to empty and AccessConfigs to nil.
1435-
// This will opt them out from being modified in the patch call.
1436-
networkInterface.NetworkIP = ""
1437-
networkInterface.AccessConfigs = nil
14381438
if !updateDuringStop && d.HasChange(prefix+".alias_ip_range") {
14391439
// Alias IP ranges cannot be updated; they must be removed and then added
14401440
// unless you are changing subnetwork/network
@@ -1459,8 +1459,11 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
14591459
instNetworkInterface = instance.NetworkInterfaces[i]
14601460
}
14611461

1462-
networkInterface.Fingerprint = instNetworkInterface.Fingerprint
1463-
updateCall := config.NewComputeBetaClient(userAgent).Instances.UpdateNetworkInterface(project, zone, instance.Name, networkName, networkInterface).Do
1462+
networkInterfacePatchObj := &computeBeta.NetworkInterface{
1463+
AliasIpRanges: networkInterface.AliasIpRanges,
1464+
Fingerprint: instNetworkInterface.Fingerprint,
1465+
}
1466+
updateCall := config.NewComputeBetaClient(userAgent).Instances.UpdateNetworkInterface(project, zone, instance.Name, networkName, networkInterfacePatchObj).Do
14641467
op, err := updateCall()
14651468
if err != nil {
14661469
return errwrap.Wrapf("Error updating network interface: {{err}}", err)
@@ -1469,9 +1472,30 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
14691472
if opErr != nil {
14701473
return opErr
14711474
}
1472-
} else if updateDuringStop {
1473-
networkInterface.Fingerprint = instNetworkInterface.Fingerprint
1474-
updateCall := config.NewComputeBetaClient(userAgent).Instances.UpdateNetworkInterface(project, zone, instance.Name, networkName, networkInterface).Do
1475+
}
1476+
1477+
if updateDuringStop {
1478+
// Lets be explicit about what we are changing in the patch call
1479+
networkInterfacePatchObj := &computeBeta.NetworkInterface{
1480+
Network: networkInterface.Network,
1481+
Subnetwork: networkInterface.Subnetwork,
1482+
AliasIpRanges: networkInterface.AliasIpRanges,
1483+
}
1484+
1485+
// network_ip can be inferred if not declared. Let's only patch if it's being changed by user
1486+
// otherwise this could fail if the network ip is not compatible with the new Subnetwork/Network.
1487+
if d.HasChange(prefix + ".network_ip") {
1488+
networkInterfacePatchObj.NetworkIP = networkInterface.NetworkIP
1489+
}
1490+
1491+
// Access config can run into some issues since we can't tell the difference between
1492+
// the users declared intent (config within their hcl file) and what we have inferred from the
1493+
// server (terraform state). Access configs contain an ip subproperty that can be incompatible
1494+
// with the subnetwork/network we are transitioning to. Due to this we only change access
1495+
// configs if we notice the configuration (user intent) changes.
1496+
accessConfigsHaveChanged := d.HasChange(prefix + ".access_config")
1497+
1498+
updateCall := computeInstanceCreateUpdateWhileStoppedCall(d, config, networkInterfacePatchObj, networkInterface.AccessConfigs, accessConfigsHaveChanged, i, project, zone, userAgent, instance.Name)
14751499
updatesToNIWhileStopped = append(updatesToNIWhileStopped, updateCall)
14761500
}
14771501
}
@@ -1737,14 +1761,18 @@ func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) err
17371761
}
17381762
}
17391763

1740-
for _, updateCall := range updatesToNIWhileStopped {
1741-
op, err := updateCall()
1764+
// If the instance stops it can invalidate the fingerprint for network interface.
1765+
// refresh the instance to get a new fingerprint
1766+
if len(updatesToNIWhileStopped) > 0 {
1767+
instance, err = config.NewComputeBetaClient(userAgent).Instances.Get(project, zone, instance.Name).Do()
17421768
if err != nil {
1743-
return errwrap.Wrapf("Error updating network interface: {{err}}", err)
1769+
return err
17441770
}
1745-
opErr := computeOperationWaitTime(config, op, project, "network interface to update", userAgent, d.Timeout(schema.TimeoutUpdate))
1746-
if opErr != nil {
1747-
return opErr
1771+
}
1772+
for _, patch := range updatesToNIWhileStopped {
1773+
err := patch(instance)
1774+
if err != nil {
1775+
return err
17481776
}
17491777
}
17501778

0 commit comments

Comments
 (0)