Skip to content

Commit 232cb87

Browse files
authored
Add versioned Beta support to google_compute_instance_group_manager (#234)
* Vendor GCP Compute Beta client library. * Refactor resource_compute_instance_group_manager for multi version support (#129) * Refactor resource_compute_instance_group_manager for multi version support. * Minor changes based on review. * Removed type-specific API version conversion functions. * Add support for Beta operations. * Add v0beta support to google_compute_instance_group_manager. * Renamed Key to Feature, added comments & updated some parameter names. * Fix code and tests for version finder to match fields that don't have a change. * Store non-v1 resources' self links as v1 so that dependent single-version resources don't see diffs. * Fix weird change to vendor.json from merge. * Add a note that Convert loses ForceSendFields, fix failing test. * Moved nil type to a switch case in compute_shared_operation.go. * Move base api version declaration above schema.
1 parent bc5505d commit 232cb87

9 files changed

+799
-78
lines changed

google/api_versions.go

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package google
2+
3+
import (
4+
"encoding/json"
5+
)
6+
7+
type ComputeApiVersion uint8
8+
9+
const (
10+
v1 ComputeApiVersion = iota
11+
v0beta
12+
)
13+
14+
var OrderedComputeApiVersions = []ComputeApiVersion{
15+
v0beta,
16+
v1,
17+
}
18+
19+
// Convert between two types by converting to/from JSON. Intended to switch
20+
// between multiple API versions, as they are strict supersets of one another.
21+
// Convert loses information about ForceSendFields and NullFields.
22+
func Convert(item, out interface{}) error {
23+
bytes, err := json.Marshal(item)
24+
if err != nil {
25+
return err
26+
}
27+
28+
err = json.Unmarshal(bytes, out)
29+
if err != nil {
30+
return err
31+
}
32+
33+
return nil
34+
}
35+
36+
type TerraformResourceData interface {
37+
HasChange(string) bool
38+
GetOk(string) (interface{}, bool)
39+
}
40+
41+
// Compare the fields set in schema against a list of features and their versions to determine
42+
// what version of the API is required in order to manage the resource.
43+
func getComputeApiVersion(d TerraformResourceData, resourceVersion ComputeApiVersion, features []Feature) ComputeApiVersion {
44+
versions := map[ComputeApiVersion]struct{}{resourceVersion: struct{}{}}
45+
for _, feature := range features {
46+
if feature.InUseBy(d) {
47+
versions[feature.Version] = struct{}{}
48+
}
49+
}
50+
51+
return maxVersion(versions)
52+
}
53+
54+
// Compare the fields set in schema against a list of features and their version, and a
55+
// list of features that exist at the base resource version that can only be update at some other
56+
// version, to determine what version of the API is required in order to update the resource.
57+
func getComputeApiVersionUpdate(d TerraformResourceData, resourceVersion ComputeApiVersion, features, updateOnlyFields []Feature) ComputeApiVersion {
58+
versions := map[ComputeApiVersion]struct{}{resourceVersion: struct{}{}}
59+
schemaVersion := getComputeApiVersion(d, resourceVersion, features)
60+
versions[schemaVersion] = struct{}{}
61+
62+
for _, feature := range updateOnlyFields {
63+
if feature.HasChangeBy(d) {
64+
versions[feature.Version] = struct{}{}
65+
}
66+
}
67+
68+
return maxVersion(versions)
69+
}
70+
71+
// A field of a resource and the version of the Compute API required to use it.
72+
type Feature struct {
73+
Version ComputeApiVersion
74+
Item string
75+
}
76+
77+
// Returns true when a feature has been modified.
78+
// This is most important when updating a resource to remove versioned feature usage; if the
79+
// resource is reverting to its base version, it needs to perform a final update at the higher
80+
// version in order to remove high version features.
81+
func (s Feature) HasChangeBy(d TerraformResourceData) bool {
82+
return d.HasChange(s.Item)
83+
}
84+
85+
// Return true when a feature appears in schema or has been modified.
86+
func (s Feature) InUseBy(d TerraformResourceData) bool {
87+
_, ok := d.GetOk(s.Item)
88+
return ok || s.HasChangeBy(d)
89+
}
90+
91+
func maxVersion(versionsInUse map[ComputeApiVersion]struct{}) ComputeApiVersion {
92+
for _, version := range OrderedComputeApiVersions {
93+
if _, ok := versionsInUse[version]; ok {
94+
return version
95+
}
96+
}
97+
98+
// Fallback to the final, most stable version
99+
return OrderedComputeApiVersions[len(OrderedComputeApiVersions)-1]
100+
}

google/api_versions_test.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package google
2+
3+
import "testing"
4+
5+
func TestResourceWithOnlyBaseVersionFields(t *testing.T) {
6+
d := &ResourceDataMock{
7+
FieldsInSchema: []string{"normal_field"},
8+
}
9+
10+
resourceVersion := v1
11+
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{})
12+
if computeApiVersion != resourceVersion {
13+
t.Errorf("Expected to see version: %v. Saw version: %v.", resourceVersion, computeApiVersion)
14+
}
15+
16+
computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{}, []Feature{})
17+
if computeApiVersion != resourceVersion {
18+
t.Errorf("Expected to see version: %v. Saw version: %v.", resourceVersion, computeApiVersion)
19+
}
20+
}
21+
22+
func TestResourceWithBetaFields(t *testing.T) {
23+
resourceVersion := v1
24+
d := &ResourceDataMock{
25+
FieldsInSchema: []string{"normal_field", "beta_field"},
26+
}
27+
28+
expectedVersion := v0beta
29+
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}})
30+
if computeApiVersion != expectedVersion {
31+
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
32+
}
33+
34+
computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}}, []Feature{})
35+
if computeApiVersion != expectedVersion {
36+
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
37+
}
38+
}
39+
40+
func TestResourceWithBetaFieldsNotInSchema(t *testing.T) {
41+
resourceVersion := v1
42+
d := &ResourceDataMock{
43+
FieldsInSchema: []string{"normal_field"},
44+
}
45+
46+
expectedVersion := v1
47+
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}})
48+
if computeApiVersion != expectedVersion {
49+
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
50+
}
51+
52+
computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}}, []Feature{})
53+
if computeApiVersion != expectedVersion {
54+
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
55+
}
56+
}
57+
58+
func TestResourceWithBetaUpdateFields(t *testing.T) {
59+
resourceVersion := v1
60+
d := &ResourceDataMock{
61+
FieldsInSchema: []string{"normal_field", "beta_update_field"},
62+
FieldsWithHasChange: []string{"beta_update_field"},
63+
}
64+
65+
expectedVersion := v1
66+
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{})
67+
if computeApiVersion != expectedVersion {
68+
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
69+
}
70+
71+
expectedVersion = v0beta
72+
computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{}, []Feature{{Version: expectedVersion, Item: "beta_update_field"}})
73+
if computeApiVersion != expectedVersion {
74+
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion)
75+
}
76+
77+
}
78+
79+
type ResourceDataMock struct {
80+
FieldsInSchema []string
81+
FieldsWithHasChange []string
82+
}
83+
84+
func (d *ResourceDataMock) HasChange(key string) bool {
85+
exists := false
86+
for _, val := range d.FieldsWithHasChange {
87+
if key == val {
88+
exists = true
89+
}
90+
}
91+
92+
return exists
93+
}
94+
95+
func (d *ResourceDataMock) GetOk(key string) (interface{}, bool) {
96+
exists := false
97+
for _, val := range d.FieldsInSchema {
98+
if key == val {
99+
exists = true
100+
}
101+
102+
}
103+
104+
return nil, exists
105+
}

google/compute_beta_operation.go

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package google
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"log"
7+
"time"
8+
9+
"github.com/hashicorp/terraform/helper/resource"
10+
11+
computeBeta "google.golang.org/api/compute/v0.beta"
12+
)
13+
14+
// OperationBetaWaitType is an enum specifying what type of operation
15+
// we're waiting on from the beta API.
16+
type ComputeBetaOperationWaitType byte
17+
18+
const (
19+
ComputeBetaOperationWaitInvalid ComputeBetaOperationWaitType = iota
20+
ComputeBetaOperationWaitGlobal
21+
ComputeBetaOperationWaitRegion
22+
ComputeBetaOperationWaitZone
23+
)
24+
25+
type ComputeBetaOperationWaiter struct {
26+
Service *computeBeta.Service
27+
Op *computeBeta.Operation
28+
Project string
29+
Region string
30+
Type ComputeBetaOperationWaitType
31+
Zone string
32+
}
33+
34+
func (w *ComputeBetaOperationWaiter) RefreshFunc() resource.StateRefreshFunc {
35+
return func() (interface{}, string, error) {
36+
var op *computeBeta.Operation
37+
var err error
38+
39+
switch w.Type {
40+
case ComputeBetaOperationWaitGlobal:
41+
op, err = w.Service.GlobalOperations.Get(
42+
w.Project, w.Op.Name).Do()
43+
case ComputeBetaOperationWaitRegion:
44+
op, err = w.Service.RegionOperations.Get(
45+
w.Project, w.Region, w.Op.Name).Do()
46+
case ComputeBetaOperationWaitZone:
47+
op, err = w.Service.ZoneOperations.Get(
48+
w.Project, w.Zone, w.Op.Name).Do()
49+
default:
50+
return nil, "bad-type", fmt.Errorf(
51+
"Invalid wait type: %#v", w.Type)
52+
}
53+
54+
if err != nil {
55+
return nil, "", err
56+
}
57+
58+
log.Printf("[DEBUG] Got %q when asking for operation %q", op.Status, w.Op.Name)
59+
60+
return op, op.Status, nil
61+
}
62+
}
63+
64+
func (w *ComputeBetaOperationWaiter) Conf() *resource.StateChangeConf {
65+
return &resource.StateChangeConf{
66+
Pending: []string{"PENDING", "RUNNING"},
67+
Target: []string{"DONE"},
68+
Refresh: w.RefreshFunc(),
69+
}
70+
}
71+
72+
// ComputeBetaOperationError wraps computeBeta.OperationError and implements the
73+
// error interface so it can be returned.
74+
type ComputeBetaOperationError computeBeta.OperationError
75+
76+
func (e ComputeBetaOperationError) Error() string {
77+
var buf bytes.Buffer
78+
79+
for _, err := range e.Errors {
80+
buf.WriteString(err.Message + "\n")
81+
}
82+
83+
return buf.String()
84+
}
85+
86+
func computeBetaOperationWaitGlobal(config *Config, op *computeBeta.Operation, project string, activity string) error {
87+
return computeBetaOperationWaitGlobalTime(config, op, project, activity, 4)
88+
}
89+
90+
func computeBetaOperationWaitGlobalTime(config *Config, op *computeBeta.Operation, project string, activity string, timeoutMin int) error {
91+
w := &ComputeBetaOperationWaiter{
92+
Service: config.clientComputeBeta,
93+
Op: op,
94+
Project: project,
95+
Type: ComputeBetaOperationWaitGlobal,
96+
}
97+
98+
state := w.Conf()
99+
state.Delay = 10 * time.Second
100+
state.Timeout = time.Duration(timeoutMin) * time.Minute
101+
state.MinTimeout = 2 * time.Second
102+
opRaw, err := state.WaitForState()
103+
if err != nil {
104+
return fmt.Errorf("Error waiting for %s: %s", activity, err)
105+
}
106+
107+
op = opRaw.(*computeBeta.Operation)
108+
if op.Error != nil {
109+
return ComputeBetaOperationError(*op.Error)
110+
}
111+
112+
return nil
113+
}
114+
115+
func computeBetaOperationWaitRegion(config *Config, op *computeBeta.Operation, project string, region, activity string) error {
116+
w := &ComputeBetaOperationWaiter{
117+
Service: config.clientComputeBeta,
118+
Op: op,
119+
Project: project,
120+
Type: ComputeBetaOperationWaitRegion,
121+
Region: region,
122+
}
123+
124+
state := w.Conf()
125+
state.Delay = 10 * time.Second
126+
state.Timeout = 4 * time.Minute
127+
state.MinTimeout = 2 * time.Second
128+
opRaw, err := state.WaitForState()
129+
if err != nil {
130+
return fmt.Errorf("Error waiting for %s: %s", activity, err)
131+
}
132+
133+
op = opRaw.(*computeBeta.Operation)
134+
if op.Error != nil {
135+
return ComputeBetaOperationError(*op.Error)
136+
}
137+
138+
return nil
139+
}
140+
141+
func computeBetaOperationWaitZone(config *Config, op *computeBeta.Operation, project string, zone, activity string) error {
142+
return computeBetaOperationWaitZoneTime(config, op, project, zone, 4, activity)
143+
}
144+
145+
func computeBetaOperationWaitZoneTime(config *Config, op *computeBeta.Operation, project string, zone string, minutes int, activity string) error {
146+
w := &ComputeBetaOperationWaiter{
147+
Service: config.clientComputeBeta,
148+
Op: op,
149+
Project: project,
150+
Zone: zone,
151+
Type: ComputeBetaOperationWaitZone,
152+
}
153+
state := w.Conf()
154+
state.Delay = 10 * time.Second
155+
state.Timeout = time.Duration(minutes) * time.Minute
156+
state.MinTimeout = 2 * time.Second
157+
opRaw, err := state.WaitForState()
158+
if err != nil {
159+
return fmt.Errorf("Error waiting for %s: %s", activity, err)
160+
}
161+
op = opRaw.(*computeBeta.Operation)
162+
if op.Error != nil {
163+
// Return the error
164+
return ComputeBetaOperationError(*op.Error)
165+
}
166+
return nil
167+
}

google/compute_shared_operation.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package google
2+
3+
import (
4+
computeBeta "google.golang.org/api/compute/v0.beta"
5+
"google.golang.org/api/compute/v1"
6+
)
7+
8+
func computeSharedOperationWaitZone(config *Config, op interface{}, project string, zone, activity string) error {
9+
return computeSharedOperationWaitZoneTime(config, op, project, zone, 4, activity)
10+
}
11+
12+
func computeSharedOperationWaitZoneTime(config *Config, op interface{}, project string, zone string, minutes int, activity string) error {
13+
switch op.(type) {
14+
case *compute.Operation:
15+
return computeOperationWaitZoneTime(config, op.(*compute.Operation), project, zone, minutes, activity)
16+
case *computeBeta.Operation:
17+
return computeBetaOperationWaitZoneTime(config, op.(*computeBeta.Operation), project, zone, minutes, activity)
18+
case nil:
19+
panic("Attempted to wait on an Operation that was nil.")
20+
default:
21+
panic("Attempted to wait on an Operation of unknown type.")
22+
}
23+
}

0 commit comments

Comments
 (0)