Skip to content

Commit 2bd786c

Browse files
Add volume replication support for Google Cloud NetApp Volumes (#9816) (#1975)
* Initial replication commit * Cleanup work - Renamed a lot of files to make clear which resource the belong to - Updated documentation for resource fields - Renamed a few resource fields and changed some types - Disabled the custom code for now. Needs to be discussed first * Update example file * Updated example file * Major updates - Reorganisation of block - Reorganisation of fields to match API documentation - Updated example parameters - Added missing API fields - Improved descriptions - * For replication deletion, stop replication first * Add support for deleting destination volume on replication delete * Make volumes deletable in presence of snapshots. This change will be PRed for volume resource independently. Adding it here while it is not in main. * Improving debug error message * yaml check and format fix * Add wait for mirror to initialize. Required to run destroy shortly after create. * Wait on destroy, not on create * Make deleting a replication more robust - doc improvements - started to implement stop/resume. More work required. - renamed a few files to better reflect what they are good for * adding support for stop/resume * yamlformat and lint * Add force delete to delete volumes with nested snapshots * resource test first version * More changes to make tests solid - Introduced new parameter to wait for mirror_status==MIRRORED - more mirror state reconciliation * Test updates * few cleanups * Make virtual field verifies happy * Minor test improvements * More fine tuning - Remove merge conflict in volume.yaml - make generated test work - make output field work - ignore_read for virtual fields * Resource name change as suggested by @slevenick * Remove snapshot code block and fix typo * Detect manual stop/resume actions * Remove ignore_read for deletion_policy * - Made destinationVolumeParameters immutable. It still requires ignore_read. - removed ignore_read from virtual_fields * destinationVolumeParameters are only evaluated at create. Make the immutable. * Name cleanups and comment improvements * removed comment * tabs to spaces in resource block * Updates to address review comments - make wait_for_mirror also work for stop/resume, additionally to create - convert tabs in test resource blocks to spaces - fix typos * Rewording of comments --------- [upstream:37fb2ebf10afb245649e86f17bc344bf7b10ef8b] Signed-off-by: Modular Magician <[email protected]>
1 parent dab97d6 commit 2bd786c

File tree

3 files changed

+218
-3
lines changed

3 files changed

+218
-3
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ require (
1111
github.com/hashicorp/hcl/v2 v2.19.1
1212
github.com/hashicorp/terraform-json v0.18.0
1313
github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0
14-
github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20240221190706-d4c89b3aba13
14+
github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20240221192239-22da542ba88e
1515
github.com/mitchellh/go-homedir v1.1.0 // indirect
1616
github.com/pkg/errors v0.9.1
1717
github.com/stretchr/testify v1.8.4

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwU
175175
github.com/hashicorp/terraform-plugin-mux v0.13.0 h1:79U401/3nd8CWwDGtTHc8F3miSCAS9XGtVarxSTDgwA=
176176
github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0 h1:Bl3e2ei2j/Z3Hc2HIS15Gal2KMKyLAZ2om1HCEvK6es=
177177
github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0/go.mod h1:i2C41tszDjiWfziPQDL5R/f3Zp0gahXe5No/MIO9rCE=
178-
github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20240221190706-d4c89b3aba13 h1:ZidAcWK/N2f0DqDydKex+/VpMFMxFsv91POuYZ5hVp4=
179-
github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20240221190706-d4c89b3aba13/go.mod h1:iQJGQHx40qba9wE8pOGbItSL5G+XRunVeL6RFRJTNcE=
178+
github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20240221192239-22da542ba88e h1:ipgLIQsEdGqzcUrBq6m2d+j94qnnTlVivCNwAsoOdW0=
179+
github.com/hashicorp/terraform-provider-google-beta v1.20.1-0.20240221192239-22da542ba88e/go.mod h1:iQJGQHx40qba9wE8pOGbItSL5G+XRunVeL6RFRJTNcE=
180180
github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
181181
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
182182
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// ----------------------------------------------------------------------------
2+
//
3+
// *** AUTO GENERATED CODE *** Type: MMv1 ***
4+
//
5+
// ----------------------------------------------------------------------------
6+
//
7+
// This file is automatically generated by Magic Modules and manual
8+
// changes will be clobbered when the file is regenerated.
9+
//
10+
// Please read more about how to change this file in
11+
// .github/CONTRIBUTING.md.
12+
//
13+
// ----------------------------------------------------------------------------
14+
15+
package netapp
16+
17+
import (
18+
"fmt"
19+
"log"
20+
"reflect"
21+
"time"
22+
23+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
24+
25+
"github.com/GoogleCloudPlatform/terraform-google-conversion/v5/tfplan2cai/converters/google/resources/cai"
26+
"github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource"
27+
transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/transport"
28+
)
29+
30+
// Custom function to wait for mirrorState target states
31+
func NetAppVolumeReplicationWaitForMirror(d *schema.ResourceData, meta interface{}, targetState string) error {
32+
config := meta.(*transport_tpg.Config)
33+
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
34+
if err != nil {
35+
return err
36+
}
37+
38+
url, err := tpgresource.ReplaceVars(d, config, "{{NetappBasePath}}projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications/{{name}}")
39+
if err != nil {
40+
return err
41+
}
42+
43+
billingProject := ""
44+
45+
project, err := tpgresource.GetProject(d, config)
46+
if err != nil {
47+
return fmt.Errorf("Error fetching project for volume replication: %s", err)
48+
}
49+
billingProject = project
50+
51+
// err == nil indicates that the billing_project value was found
52+
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
53+
billingProject = bp
54+
}
55+
56+
for {
57+
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
58+
Config: config,
59+
Method: "GET",
60+
Project: billingProject,
61+
RawURL: url,
62+
UserAgent: userAgent,
63+
})
64+
if err != nil {
65+
return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("NetappVolumeReplication %q", d.Id()))
66+
}
67+
68+
log.Printf("[DEBUG] waiting for mirrorState. actual: %v, target: %v", res["mirrorState"], targetState)
69+
70+
if res["mirrorState"] == targetState {
71+
break
72+
}
73+
74+
time.Sleep(30 * time.Second)
75+
// This method can potentially run for days, e.g. when setting up a replication for a source volume
76+
// with dozens of TiB of data. Timeout handling yes/no?
77+
}
78+
79+
return nil
80+
}
81+
82+
const NetappVolumeReplicationAssetType string = "netapp.googleapis.com/VolumeReplication"
83+
84+
func ResourceConverterNetappVolumeReplication() cai.ResourceConverter {
85+
return cai.ResourceConverter{
86+
AssetType: NetappVolumeReplicationAssetType,
87+
Convert: GetNetappVolumeReplicationCaiObject,
88+
}
89+
}
90+
91+
func GetNetappVolumeReplicationCaiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) ([]cai.Asset, error) {
92+
name, err := cai.AssetName(d, config, "//netapp.googleapis.com/projects/{{project}}/locations/{{location}}/volumes/{{volume_name}}/replications/{{name}}")
93+
if err != nil {
94+
return []cai.Asset{}, err
95+
}
96+
if obj, err := GetNetappVolumeReplicationApiObject(d, config); err == nil {
97+
return []cai.Asset{{
98+
Name: name,
99+
Type: NetappVolumeReplicationAssetType,
100+
Resource: &cai.AssetResource{
101+
Version: "v1beta1",
102+
DiscoveryDocumentURI: "https://www.googleapis.com/discovery/v1/apis/netapp/v1beta1/rest",
103+
DiscoveryName: "VolumeReplication",
104+
Data: obj,
105+
},
106+
}}, nil
107+
} else {
108+
return []cai.Asset{}, err
109+
}
110+
}
111+
112+
func GetNetappVolumeReplicationApiObject(d tpgresource.TerraformResourceData, config *transport_tpg.Config) (map[string]interface{}, error) {
113+
obj := make(map[string]interface{})
114+
replicationScheduleProp, err := expandNetappVolumeReplicationReplicationSchedule(d.Get("replication_schedule"), d, config)
115+
if err != nil {
116+
return nil, err
117+
} else if v, ok := d.GetOkExists("replication_schedule"); !tpgresource.IsEmptyValue(reflect.ValueOf(replicationScheduleProp)) && (ok || !reflect.DeepEqual(v, replicationScheduleProp)) {
118+
obj["replicationSchedule"] = replicationScheduleProp
119+
}
120+
destinationVolumeParametersProp, err := expandNetappVolumeReplicationDestinationVolumeParameters(d.Get("destination_volume_parameters"), d, config)
121+
if err != nil {
122+
return nil, err
123+
} else if v, ok := d.GetOkExists("destination_volume_parameters"); !tpgresource.IsEmptyValue(reflect.ValueOf(destinationVolumeParametersProp)) && (ok || !reflect.DeepEqual(v, destinationVolumeParametersProp)) {
124+
obj["destinationVolumeParameters"] = destinationVolumeParametersProp
125+
}
126+
descriptionProp, err := expandNetappVolumeReplicationDescription(d.Get("description"), d, config)
127+
if err != nil {
128+
return nil, err
129+
} else if v, ok := d.GetOkExists("description"); !tpgresource.IsEmptyValue(reflect.ValueOf(descriptionProp)) && (ok || !reflect.DeepEqual(v, descriptionProp)) {
130+
obj["description"] = descriptionProp
131+
}
132+
labelsProp, err := expandNetappVolumeReplicationEffectiveLabels(d.Get("effective_labels"), d, config)
133+
if err != nil {
134+
return nil, err
135+
} else if v, ok := d.GetOkExists("effective_labels"); !tpgresource.IsEmptyValue(reflect.ValueOf(labelsProp)) && (ok || !reflect.DeepEqual(v, labelsProp)) {
136+
obj["labels"] = labelsProp
137+
}
138+
139+
return obj, nil
140+
}
141+
142+
func expandNetappVolumeReplicationReplicationSchedule(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
143+
return v, nil
144+
}
145+
146+
func expandNetappVolumeReplicationDestinationVolumeParameters(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
147+
l := v.([]interface{})
148+
if len(l) == 0 || l[0] == nil {
149+
return nil, nil
150+
}
151+
raw := l[0]
152+
original := raw.(map[string]interface{})
153+
transformed := make(map[string]interface{})
154+
155+
transformedStoragePool, err := expandNetappVolumeReplicationDestinationVolumeParametersStoragePool(original["storage_pool"], d, config)
156+
if err != nil {
157+
return nil, err
158+
} else if val := reflect.ValueOf(transformedStoragePool); val.IsValid() && !tpgresource.IsEmptyValue(val) {
159+
transformed["storagePool"] = transformedStoragePool
160+
}
161+
162+
transformedVolumeId, err := expandNetappVolumeReplicationDestinationVolumeParametersVolumeId(original["volume_id"], d, config)
163+
if err != nil {
164+
return nil, err
165+
} else if val := reflect.ValueOf(transformedVolumeId); val.IsValid() && !tpgresource.IsEmptyValue(val) {
166+
transformed["volumeId"] = transformedVolumeId
167+
}
168+
169+
transformedShareName, err := expandNetappVolumeReplicationDestinationVolumeParametersShareName(original["share_name"], d, config)
170+
if err != nil {
171+
return nil, err
172+
} else if val := reflect.ValueOf(transformedShareName); val.IsValid() && !tpgresource.IsEmptyValue(val) {
173+
transformed["shareName"] = transformedShareName
174+
}
175+
176+
transformedDescription, err := expandNetappVolumeReplicationDestinationVolumeParametersDescription(original["description"], d, config)
177+
if err != nil {
178+
return nil, err
179+
} else if val := reflect.ValueOf(transformedDescription); val.IsValid() && !tpgresource.IsEmptyValue(val) {
180+
transformed["description"] = transformedDescription
181+
}
182+
183+
return transformed, nil
184+
}
185+
186+
func expandNetappVolumeReplicationDestinationVolumeParametersStoragePool(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
187+
return v, nil
188+
}
189+
190+
func expandNetappVolumeReplicationDestinationVolumeParametersVolumeId(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
191+
return v, nil
192+
}
193+
194+
func expandNetappVolumeReplicationDestinationVolumeParametersShareName(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
195+
return v, nil
196+
}
197+
198+
func expandNetappVolumeReplicationDestinationVolumeParametersDescription(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
199+
return v, nil
200+
}
201+
202+
func expandNetappVolumeReplicationDescription(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
203+
return v, nil
204+
}
205+
206+
func expandNetappVolumeReplicationEffectiveLabels(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (map[string]string, error) {
207+
if v == nil {
208+
return map[string]string{}, nil
209+
}
210+
m := make(map[string]string)
211+
for k, val := range v.(map[string]interface{}) {
212+
m[k] = val.(string)
213+
}
214+
return m, nil
215+
}

0 commit comments

Comments
 (0)