-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
bc5505d
commit 232cb87
Showing
9 changed files
with
799 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
package google | ||
|
||
import ( | ||
"encoding/json" | ||
) | ||
|
||
type ComputeApiVersion uint8 | ||
|
||
const ( | ||
v1 ComputeApiVersion = iota | ||
v0beta | ||
) | ||
|
||
var OrderedComputeApiVersions = []ComputeApiVersion{ | ||
v0beta, | ||
v1, | ||
} | ||
|
||
// Convert between two types by converting to/from JSON. Intended to switch | ||
// between multiple API versions, as they are strict supersets of one another. | ||
// Convert loses information about ForceSendFields and NullFields. | ||
func Convert(item, out interface{}) error { | ||
bytes, err := json.Marshal(item) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = json.Unmarshal(bytes, out) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type TerraformResourceData interface { | ||
HasChange(string) bool | ||
GetOk(string) (interface{}, bool) | ||
} | ||
|
||
// Compare the fields set in schema against a list of features and their versions to determine | ||
// what version of the API is required in order to manage the resource. | ||
func getComputeApiVersion(d TerraformResourceData, resourceVersion ComputeApiVersion, features []Feature) ComputeApiVersion { | ||
versions := map[ComputeApiVersion]struct{}{resourceVersion: struct{}{}} | ||
for _, feature := range features { | ||
if feature.InUseBy(d) { | ||
versions[feature.Version] = struct{}{} | ||
} | ||
} | ||
|
||
return maxVersion(versions) | ||
} | ||
|
||
// Compare the fields set in schema against a list of features and their version, and a | ||
// list of features that exist at the base resource version that can only be update at some other | ||
// version, to determine what version of the API is required in order to update the resource. | ||
func getComputeApiVersionUpdate(d TerraformResourceData, resourceVersion ComputeApiVersion, features, updateOnlyFields []Feature) ComputeApiVersion { | ||
versions := map[ComputeApiVersion]struct{}{resourceVersion: struct{}{}} | ||
schemaVersion := getComputeApiVersion(d, resourceVersion, features) | ||
versions[schemaVersion] = struct{}{} | ||
|
||
for _, feature := range updateOnlyFields { | ||
if feature.HasChangeBy(d) { | ||
versions[feature.Version] = struct{}{} | ||
} | ||
} | ||
|
||
return maxVersion(versions) | ||
} | ||
|
||
// A field of a resource and the version of the Compute API required to use it. | ||
type Feature struct { | ||
Version ComputeApiVersion | ||
Item string | ||
} | ||
|
||
// Returns true when a feature has been modified. | ||
// This is most important when updating a resource to remove versioned feature usage; if the | ||
// resource is reverting to its base version, it needs to perform a final update at the higher | ||
// version in order to remove high version features. | ||
func (s Feature) HasChangeBy(d TerraformResourceData) bool { | ||
return d.HasChange(s.Item) | ||
} | ||
|
||
// Return true when a feature appears in schema or has been modified. | ||
func (s Feature) InUseBy(d TerraformResourceData) bool { | ||
_, ok := d.GetOk(s.Item) | ||
return ok || s.HasChangeBy(d) | ||
} | ||
|
||
func maxVersion(versionsInUse map[ComputeApiVersion]struct{}) ComputeApiVersion { | ||
for _, version := range OrderedComputeApiVersions { | ||
if _, ok := versionsInUse[version]; ok { | ||
return version | ||
} | ||
} | ||
|
||
// Fallback to the final, most stable version | ||
return OrderedComputeApiVersions[len(OrderedComputeApiVersions)-1] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package google | ||
|
||
import "testing" | ||
|
||
func TestResourceWithOnlyBaseVersionFields(t *testing.T) { | ||
d := &ResourceDataMock{ | ||
FieldsInSchema: []string{"normal_field"}, | ||
} | ||
|
||
resourceVersion := v1 | ||
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{}) | ||
if computeApiVersion != resourceVersion { | ||
t.Errorf("Expected to see version: %v. Saw version: %v.", resourceVersion, computeApiVersion) | ||
} | ||
|
||
computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{}, []Feature{}) | ||
if computeApiVersion != resourceVersion { | ||
t.Errorf("Expected to see version: %v. Saw version: %v.", resourceVersion, computeApiVersion) | ||
} | ||
} | ||
|
||
func TestResourceWithBetaFields(t *testing.T) { | ||
resourceVersion := v1 | ||
d := &ResourceDataMock{ | ||
FieldsInSchema: []string{"normal_field", "beta_field"}, | ||
} | ||
|
||
expectedVersion := v0beta | ||
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}}) | ||
if computeApiVersion != expectedVersion { | ||
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion) | ||
} | ||
|
||
computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}}, []Feature{}) | ||
if computeApiVersion != expectedVersion { | ||
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion) | ||
} | ||
} | ||
|
||
func TestResourceWithBetaFieldsNotInSchema(t *testing.T) { | ||
resourceVersion := v1 | ||
d := &ResourceDataMock{ | ||
FieldsInSchema: []string{"normal_field"}, | ||
} | ||
|
||
expectedVersion := v1 | ||
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}}) | ||
if computeApiVersion != expectedVersion { | ||
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion) | ||
} | ||
|
||
computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{{Version: expectedVersion, Item: "beta_field"}}, []Feature{}) | ||
if computeApiVersion != expectedVersion { | ||
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion) | ||
} | ||
} | ||
|
||
func TestResourceWithBetaUpdateFields(t *testing.T) { | ||
resourceVersion := v1 | ||
d := &ResourceDataMock{ | ||
FieldsInSchema: []string{"normal_field", "beta_update_field"}, | ||
FieldsWithHasChange: []string{"beta_update_field"}, | ||
} | ||
|
||
expectedVersion := v1 | ||
computeApiVersion := getComputeApiVersion(d, resourceVersion, []Feature{}) | ||
if computeApiVersion != expectedVersion { | ||
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion) | ||
} | ||
|
||
expectedVersion = v0beta | ||
computeApiVersion = getComputeApiVersionUpdate(d, resourceVersion, []Feature{}, []Feature{{Version: expectedVersion, Item: "beta_update_field"}}) | ||
if computeApiVersion != expectedVersion { | ||
t.Errorf("Expected to see version: %v. Saw version: %v.", expectedVersion, computeApiVersion) | ||
} | ||
|
||
} | ||
|
||
type ResourceDataMock struct { | ||
FieldsInSchema []string | ||
FieldsWithHasChange []string | ||
} | ||
|
||
func (d *ResourceDataMock) HasChange(key string) bool { | ||
exists := false | ||
for _, val := range d.FieldsWithHasChange { | ||
if key == val { | ||
exists = true | ||
} | ||
} | ||
|
||
return exists | ||
} | ||
|
||
func (d *ResourceDataMock) GetOk(key string) (interface{}, bool) { | ||
exists := false | ||
for _, val := range d.FieldsInSchema { | ||
if key == val { | ||
exists = true | ||
} | ||
|
||
} | ||
|
||
return nil, exists | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
package google | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"log" | ||
"time" | ||
|
||
"github.com/hashicorp/terraform/helper/resource" | ||
|
||
computeBeta "google.golang.org/api/compute/v0.beta" | ||
) | ||
|
||
// OperationBetaWaitType is an enum specifying what type of operation | ||
// we're waiting on from the beta API. | ||
type ComputeBetaOperationWaitType byte | ||
|
||
const ( | ||
ComputeBetaOperationWaitInvalid ComputeBetaOperationWaitType = iota | ||
ComputeBetaOperationWaitGlobal | ||
ComputeBetaOperationWaitRegion | ||
ComputeBetaOperationWaitZone | ||
) | ||
|
||
type ComputeBetaOperationWaiter struct { | ||
Service *computeBeta.Service | ||
Op *computeBeta.Operation | ||
Project string | ||
Region string | ||
Type ComputeBetaOperationWaitType | ||
Zone string | ||
} | ||
|
||
func (w *ComputeBetaOperationWaiter) RefreshFunc() resource.StateRefreshFunc { | ||
return func() (interface{}, string, error) { | ||
var op *computeBeta.Operation | ||
var err error | ||
|
||
switch w.Type { | ||
case ComputeBetaOperationWaitGlobal: | ||
op, err = w.Service.GlobalOperations.Get( | ||
w.Project, w.Op.Name).Do() | ||
case ComputeBetaOperationWaitRegion: | ||
op, err = w.Service.RegionOperations.Get( | ||
w.Project, w.Region, w.Op.Name).Do() | ||
case ComputeBetaOperationWaitZone: | ||
op, err = w.Service.ZoneOperations.Get( | ||
w.Project, w.Zone, w.Op.Name).Do() | ||
default: | ||
return nil, "bad-type", fmt.Errorf( | ||
"Invalid wait type: %#v", w.Type) | ||
} | ||
|
||
if err != nil { | ||
return nil, "", err | ||
} | ||
|
||
log.Printf("[DEBUG] Got %q when asking for operation %q", op.Status, w.Op.Name) | ||
|
||
return op, op.Status, nil | ||
} | ||
} | ||
|
||
func (w *ComputeBetaOperationWaiter) Conf() *resource.StateChangeConf { | ||
return &resource.StateChangeConf{ | ||
Pending: []string{"PENDING", "RUNNING"}, | ||
Target: []string{"DONE"}, | ||
Refresh: w.RefreshFunc(), | ||
} | ||
} | ||
|
||
// ComputeBetaOperationError wraps computeBeta.OperationError and implements the | ||
// error interface so it can be returned. | ||
type ComputeBetaOperationError computeBeta.OperationError | ||
|
||
func (e ComputeBetaOperationError) Error() string { | ||
var buf bytes.Buffer | ||
|
||
for _, err := range e.Errors { | ||
buf.WriteString(err.Message + "\n") | ||
} | ||
|
||
return buf.String() | ||
} | ||
|
||
func computeBetaOperationWaitGlobal(config *Config, op *computeBeta.Operation, project string, activity string) error { | ||
return computeBetaOperationWaitGlobalTime(config, op, project, activity, 4) | ||
} | ||
|
||
func computeBetaOperationWaitGlobalTime(config *Config, op *computeBeta.Operation, project string, activity string, timeoutMin int) error { | ||
w := &ComputeBetaOperationWaiter{ | ||
Service: config.clientComputeBeta, | ||
Op: op, | ||
Project: project, | ||
Type: ComputeBetaOperationWaitGlobal, | ||
} | ||
|
||
state := w.Conf() | ||
state.Delay = 10 * time.Second | ||
state.Timeout = time.Duration(timeoutMin) * time.Minute | ||
state.MinTimeout = 2 * time.Second | ||
opRaw, err := state.WaitForState() | ||
if err != nil { | ||
return fmt.Errorf("Error waiting for %s: %s", activity, err) | ||
} | ||
|
||
op = opRaw.(*computeBeta.Operation) | ||
if op.Error != nil { | ||
return ComputeBetaOperationError(*op.Error) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func computeBetaOperationWaitRegion(config *Config, op *computeBeta.Operation, project string, region, activity string) error { | ||
w := &ComputeBetaOperationWaiter{ | ||
Service: config.clientComputeBeta, | ||
Op: op, | ||
Project: project, | ||
Type: ComputeBetaOperationWaitRegion, | ||
Region: region, | ||
} | ||
|
||
state := w.Conf() | ||
state.Delay = 10 * time.Second | ||
state.Timeout = 4 * time.Minute | ||
state.MinTimeout = 2 * time.Second | ||
opRaw, err := state.WaitForState() | ||
if err != nil { | ||
return fmt.Errorf("Error waiting for %s: %s", activity, err) | ||
} | ||
|
||
op = opRaw.(*computeBeta.Operation) | ||
if op.Error != nil { | ||
return ComputeBetaOperationError(*op.Error) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func computeBetaOperationWaitZone(config *Config, op *computeBeta.Operation, project string, zone, activity string) error { | ||
return computeBetaOperationWaitZoneTime(config, op, project, zone, 4, activity) | ||
} | ||
|
||
func computeBetaOperationWaitZoneTime(config *Config, op *computeBeta.Operation, project string, zone string, minutes int, activity string) error { | ||
w := &ComputeBetaOperationWaiter{ | ||
Service: config.clientComputeBeta, | ||
Op: op, | ||
Project: project, | ||
Zone: zone, | ||
Type: ComputeBetaOperationWaitZone, | ||
} | ||
state := w.Conf() | ||
state.Delay = 10 * time.Second | ||
state.Timeout = time.Duration(minutes) * time.Minute | ||
state.MinTimeout = 2 * time.Second | ||
opRaw, err := state.WaitForState() | ||
if err != nil { | ||
return fmt.Errorf("Error waiting for %s: %s", activity, err) | ||
} | ||
op = opRaw.(*computeBeta.Operation) | ||
if op.Error != nil { | ||
// Return the error | ||
return ComputeBetaOperationError(*op.Error) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package google | ||
|
||
import ( | ||
computeBeta "google.golang.org/api/compute/v0.beta" | ||
"google.golang.org/api/compute/v1" | ||
) | ||
|
||
func computeSharedOperationWaitZone(config *Config, op interface{}, project string, zone, activity string) error { | ||
return computeSharedOperationWaitZoneTime(config, op, project, zone, 4, activity) | ||
} | ||
|
||
func computeSharedOperationWaitZoneTime(config *Config, op interface{}, project string, zone string, minutes int, activity string) error { | ||
switch op.(type) { | ||
case *compute.Operation: | ||
return computeOperationWaitZoneTime(config, op.(*compute.Operation), project, zone, minutes, activity) | ||
case *computeBeta.Operation: | ||
return computeBetaOperationWaitZoneTime(config, op.(*computeBeta.Operation), project, zone, minutes, activity) | ||
case nil: | ||
panic("Attempted to wait on an Operation that was nil.") | ||
default: | ||
panic("Attempted to wait on an Operation of unknown type.") | ||
} | ||
} |
Oops, something went wrong.